diff --git a/README.md b/README.md
index 5501d11f6a..adf7845074 100644
--- a/README.md
+++ b/README.md
@@ -40,8 +40,10 @@ addon | version | maintainers | summary
[fs_storage_backup](fs_storage_backup/) | 16.0.2.0.0 | | Filesystem Storage Backup
[fs_storage_backup_environment](fs_storage_backup_environment/) | 16.0.1.0.0 | | Allows to use server environment with fs storage attachment
[fs_storage_environment](fs_storage_environment/) | 16.0.1.0.0 | | Allows to use server environment with fs storage
-[image_tag](image_tag/) | 16.0.1.0.1 | | Image tag model
+[image_tag](image_tag/) | 16.0.2.0.0 | | Image tag model
+[image_tag_environment](image_tag_environment/) | 16.0.2.0.0 | | Server environment features for the Image Tag model
[storage_backend](storage_backend/) | 16.0.1.1.0 | | Implement the concept of Storage with amazon S3, sftp...
+[storage_backend_ftp](storage_backend_ftp/) | 16.0.1.0.0 | | Implement FTP Storage
[storage_backend_sftp](storage_backend_sftp/) | 16.0.1.0.2 | | Implement SFTP Storage
[storage_file](storage_file/) | 16.0.1.0.1 | | Storage file in storage backend
diff --git a/image_tag/README.rst b/image_tag/README.rst
index 81ab69688d..3be438dc6d 100644
--- a/image_tag/README.rst
+++ b/image_tag/README.rst
@@ -1,3 +1,7 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
=========
Image Tag
=========
@@ -7,13 +11,13 @@ Image Tag
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:05203837238360e0c011f8ed825bbf70e20f56a5157f18585a37353e9a148a86
+ !! source digest: sha256:9a9aa1210e00d5861a492c284df7dd7c5b975689b83cdaef8f98a15768340cf1
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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
+.. |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
diff --git a/image_tag/__manifest__.py b/image_tag/__manifest__.py
index 0bbe310d2d..e71666e992 100644
--- a/image_tag/__manifest__.py
+++ b/image_tag/__manifest__.py
@@ -5,11 +5,11 @@
"name": "Image Tag",
"summary": """
Image tag model""",
- "version": "16.0.1.0.1",
+ "version": "16.0.2.0.0",
"license": "AGPL-3",
"author": "ACSONE SA/NV,Akretion,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/storage",
- "depends": ["server_environment"],
+ "depends": ["base"],
"data": [
"security/res_groups.xml",
"security/image_tag.xml",
diff --git a/image_tag/i18n/es.po b/image_tag/i18n/es.po
index 36459bc555..026a3c19bd 100644
--- a/image_tag/i18n/es.po
+++ b/image_tag/i18n/es.po
@@ -73,18 +73,9 @@ msgstr "Última Actualización el"
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"
+#~ 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."
+#~ 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/image_tag/i18n/image_tag.pot b/image_tag/i18n/image_tag.pot
index 9229bba009..8d9040591c 100644
--- a/image_tag/i18n/image_tag.pot
+++ b/image_tag/i18n/image_tag.pot
@@ -69,18 +69,3 @@ msgstr ""
#: 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/image_tag/i18n/it.po b/image_tag/i18n/it.po
index 3d4d2f19d0..b61b9cf738 100644
--- a/image_tag/i18n/it.po
+++ b/image_tag/i18n/it.po
@@ -73,17 +73,11 @@ msgstr "Ultimo aggiornamento il"
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"
+#~ 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"
+#~ 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."
+#~ msgid "Unique name for technical purposes. Eg: server env keys."
+#~ msgstr "Nome univoco per motivi tecnici. Es: chiavi server ambiente."
diff --git a/image_tag/models/image_tag.py b/image_tag/models/image_tag.py
index 86f255873d..847d4e1544 100644
--- a/image_tag/models/image_tag.py
+++ b/image_tag/models/image_tag.py
@@ -8,7 +8,6 @@
class ImageTag(models.Model):
_name = "image.tag"
- _inherit = ["server.env.techname.mixin"]
_description = "Image Tag"
@api.model
diff --git a/image_tag/static/description/index.html b/image_tag/static/description/index.html
index a37630a2ff..3003169efd 100644
--- a/image_tag/static/description/index.html
+++ b/image_tag/static/description/index.html
@@ -1,18 +1,18 @@
-
-Image Tag
+README.rst
-
-
Image Tag
+
+
+
+
+
+
+
Image Tag
-

+

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.
@@ -387,12 +392,12 @@
Image Tag
-
+
To manage the list of available tags, you must have the Image Tag Manager
role.
-
+
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
@@ -400,24 +405,26 @@
Do not contact contributors directly about support or help with technical issues.
-
+
-
+
This module is maintained by the OCA.
-

+
+
+
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.
@@ -426,5 +433,6 @@
+
diff --git a/image_tag/upgrades/16.0.1.0.2/post-update.py b/image_tag/upgrades/16.0.1.0.2/post-update.py
new file mode 100644
index 0000000000..574c6795b0
--- /dev/null
+++ b/image_tag/upgrades/16.0.1.0.2/post-update.py
@@ -0,0 +1,13 @@
+# Copyright 2026 Camptocamp SA
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from openupgradelib import openupgrade
+
+
+@openupgrade.migrate()
+def migrate(env, version):
+ module = env["ir.module.module"].search(
+ [("name", "=", "image_tag_environment"), ("state", "=", "uninstalled")]
+ )
+ if module:
+ module.button_install()
diff --git a/image_tag/views/image_tag.xml b/image_tag/views/image_tag.xml
index 6ecda60972..18411f7c4b 100644
--- a/image_tag/views/image_tag.xml
+++ b/image_tag/views/image_tag.xml
@@ -5,7 +5,6 @@
-
@@ -15,7 +14,6 @@
-
diff --git a/image_tag_environment/README.rst b/image_tag_environment/README.rst
new file mode 100644
index 0000000000..fc8b7541df
--- /dev/null
+++ b/image_tag_environment/README.rst
@@ -0,0 +1,87 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+==============================
+Image Tag - Server Environment
+==============================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:20e304c07b50e37319543a302422d4bae8d9b6e234f6c8138b12e938a68c2ab9
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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/image_tag_environment
+ :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_environment
+ :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|
+
+Glue module to make Server Environment features available for the Image Tag addon.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Usage
+=====
+
+Specify technical name during the creation of an image tag.
+
+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
+* Maksym Yankin
+
+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/image_tag_environment/__init__.py b/image_tag_environment/__init__.py
new file mode 100644
index 0000000000..0650744f6b
--- /dev/null
+++ b/image_tag_environment/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/image_tag_environment/__manifest__.py b/image_tag_environment/__manifest__.py
new file mode 100644
index 0000000000..1ed162d063
--- /dev/null
+++ b/image_tag_environment/__manifest__.py
@@ -0,0 +1,16 @@
+# Copyright 2023 ACSONE SA/NV
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+{
+ "name": "Image Tag - Server Environment",
+ "summary": "Server environment features for the Image Tag model",
+ "version": "16.0.2.0.0",
+ "license": "AGPL-3",
+ "author": "ACSONE SA/NV,Akretion,Odoo Community Association (OCA)",
+ "website": "https://github.com/OCA/storage",
+ "depends": ["image_tag", "server_environment"],
+ "data": [
+ "views/image_tag.xml",
+ ],
+ "auto_install": True,
+}
diff --git a/image_tag_environment/i18n/es.po b/image_tag_environment/i18n/es.po
new file mode 100644
index 0000000000..4c41eb35cd
--- /dev/null
+++ b/image_tag_environment/i18n/es.po
@@ -0,0 +1,68 @@
+# 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_environment
+#: model:ir.model,name:image_tag_environment.model_image_tag
+msgid "Image Tag"
+msgstr "Etiqueta de Imagen"
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__server_env_defaults
+msgid "Server Env Defaults"
+msgstr ""
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__tech_name
+msgid "Tech Name"
+msgstr "Nombre Técnico"
+
+#. module: image_tag_environment
+#: model:ir.model.fields,help:image_tag_environment.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."
+
+#~ msgid "Apply On"
+#~ msgstr "Aplicar En"
+
+#~ msgid "Created by"
+#~ msgstr "Creado por"
+
+#~ msgid "Created on"
+#~ msgstr "Creado el"
+
+#~ msgid "Display Name"
+#~ msgstr "Mostrar Nombre"
+
+#~ msgid "ID"
+#~ msgstr "ID (identificación)"
+
+#~ msgid "Image Tag Manager"
+#~ msgstr "Gestor de Etiquetas de Imagen"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Última Modifiación el"
+
+#~ msgid "Last Updated by"
+#~ msgstr "Actualizado por Última vez por"
+
+#~ msgid "Last Updated on"
+#~ msgstr "Última Actualización el"
+
+#~ msgid "Name"
+#~ msgstr "Nombre"
diff --git a/image_tag_environment/i18n/image_tag.pot b/image_tag_environment/i18n/image_tag.pot
new file mode 100644
index 0000000000..9229bba009
--- /dev/null
+++ b/image_tag_environment/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/image_tag_environment/i18n/image_tag_environment.pot b/image_tag_environment/i18n/image_tag_environment.pot
new file mode 100644
index 0000000000..fad2502af0
--- /dev/null
+++ b/image_tag_environment/i18n/image_tag_environment.pot
@@ -0,0 +1,34 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * image_tag_environment
+#
+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_environment
+#: model:ir.model,name:image_tag_environment.model_image_tag
+msgid "Image Tag"
+msgstr ""
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__server_env_defaults
+msgid "Server Env Defaults"
+msgstr ""
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__tech_name
+msgid "Tech Name"
+msgstr ""
+
+#. module: image_tag_environment
+#: model:ir.model.fields,help:image_tag_environment.field_image_tag__tech_name
+msgid "Unique name for technical purposes. Eg: server env keys."
+msgstr ""
diff --git a/image_tag_environment/i18n/it.po b/image_tag_environment/i18n/it.po
new file mode 100644
index 0000000000..76dcaea580
--- /dev/null
+++ b/image_tag_environment/i18n/it.po
@@ -0,0 +1,67 @@
+# 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_environment
+#: model:ir.model,name:image_tag_environment.model_image_tag
+msgid "Image Tag"
+msgstr "Etichetta immagine"
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__server_env_defaults
+msgid "Server Env Defaults"
+msgstr "Predefiniti ambiente server"
+
+#. module: image_tag_environment
+#: model:ir.model.fields,field_description:image_tag_environment.field_image_tag__tech_name
+msgid "Tech Name"
+msgstr "Nome tecnico"
+
+#. module: image_tag_environment
+#: model:ir.model.fields,help:image_tag_environment.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."
+
+#~ msgid "Apply On"
+#~ msgstr "Applica a"
+
+#~ msgid "Created by"
+#~ msgstr "Creato da"
+
+#~ msgid "Created on"
+#~ msgstr "Creato il"
+
+#~ msgid "Display Name"
+#~ msgstr "Nome visualizzato"
+
+#~ msgid "ID"
+#~ msgstr "ID"
+
+#~ msgid "Image Tag Manager"
+#~ msgstr "Gestore etichetta immagine"
+
+#~ msgid "Last Modified on"
+#~ msgstr "Ultima modifica il"
+
+#~ msgid "Last Updated by"
+#~ msgstr "Ultimo aggiornamento di"
+
+#~ msgid "Last Updated on"
+#~ msgstr "Ultimo aggiornamento il"
+
+#~ msgid "Name"
+#~ msgstr "Nome"
diff --git a/image_tag_environment/models/__init__.py b/image_tag_environment/models/__init__.py
new file mode 100644
index 0000000000..888490ab30
--- /dev/null
+++ b/image_tag_environment/models/__init__.py
@@ -0,0 +1 @@
+from . import image_tag
diff --git a/image_tag_environment/models/image_tag.py b/image_tag_environment/models/image_tag.py
new file mode 100644
index 0000000000..2c50349ff8
--- /dev/null
+++ b/image_tag_environment/models/image_tag.py
@@ -0,0 +1,11 @@
+# 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 models
+
+
+class ImageTag(models.Model):
+ _name = "image.tag"
+ _inherit = ["image.tag", "server.env.techname.mixin"]
diff --git a/image_tag_environment/readme/CONTRIBUTORS.rst b/image_tag_environment/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..0f79cbc864
--- /dev/null
+++ b/image_tag_environment/readme/CONTRIBUTORS.rst
@@ -0,0 +1,2 @@
+* Laurent Mignon
+* Maksym Yankin
diff --git a/image_tag_environment/readme/DESCRIPTION.rst b/image_tag_environment/readme/DESCRIPTION.rst
new file mode 100644
index 0000000000..4ddadc72f6
--- /dev/null
+++ b/image_tag_environment/readme/DESCRIPTION.rst
@@ -0,0 +1 @@
+Glue module to make Server Environment features available for the Image Tag addon.
diff --git a/image_tag_environment/readme/USAGE.rst b/image_tag_environment/readme/USAGE.rst
new file mode 100644
index 0000000000..b57527eec8
--- /dev/null
+++ b/image_tag_environment/readme/USAGE.rst
@@ -0,0 +1 @@
+Specify technical name during the creation of an image tag.
diff --git a/image_tag_environment/static/description/icon.png b/image_tag_environment/static/description/icon.png
new file mode 100644
index 0000000000..3a0328b516
Binary files /dev/null and b/image_tag_environment/static/description/icon.png differ
diff --git a/image_tag_environment/static/description/index.html b/image_tag_environment/static/description/index.html
new file mode 100644
index 0000000000..5b9b78aaaa
--- /dev/null
+++ b/image_tag_environment/static/description/index.html
@@ -0,0 +1,436 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+
Image Tag - Server Environment
+
+

+
Glue module to make Server Environment features available for the Image Tag addon.
+
Table of contents
+
+
+
+
Specify technical name during the creation of an image tag.
+
+
+
+
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.
+
+
+
+
+
+
+- ACSONE SA/NV
+- Akretion
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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/image_tag_environment/views/image_tag.xml b/image_tag_environment/views/image_tag.xml
new file mode 100644
index 0000000000..b1409ab2b0
--- /dev/null
+++ b/image_tag_environment/views/image_tag.xml
@@ -0,0 +1,21 @@
+
+
+
+ image.tag
+
+
+
+
+
+
+
+
+ image.tag
+
+
+
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt
index 275c9dfb40..1ec18d7bcc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,5 @@ fsspec>=2024.5.0
fsspec>=2025.3.0
fsspec[s3]
paramiko
+pyftpdlib
python_slugify
diff --git a/setup/_metapackage/VERSION.txt b/setup/_metapackage/VERSION.txt
index f2eb63f0a9..e3e99b0c1a 100644
--- a/setup/_metapackage/VERSION.txt
+++ b/setup/_metapackage/VERSION.txt
@@ -1 +1 @@
-16.0.20260608.0
\ No newline at end of file
+16.0.20260820.1
\ No newline at end of file
diff --git a/setup/_metapackage/setup.py b/setup/_metapackage/setup.py
index e7a0f2cba5..312f6ce903 100644
--- a/setup/_metapackage/setup.py
+++ b/setup/_metapackage/setup.py
@@ -27,7 +27,9 @@
'odoo-addon-fs_storage_backup_environment>=16.0dev,<16.1dev',
'odoo-addon-fs_storage_environment>=16.0dev,<16.1dev',
'odoo-addon-image_tag>=16.0dev,<16.1dev',
+ 'odoo-addon-image_tag_environment>=16.0dev,<16.1dev',
'odoo-addon-storage_backend>=16.0dev,<16.1dev',
+ 'odoo-addon-storage_backend_ftp>=16.0dev,<16.1dev',
'odoo-addon-storage_backend_sftp>=16.0dev,<16.1dev',
'odoo-addon-storage_file>=16.0dev,<16.1dev',
],
diff --git a/setup/image_tag_environment/odoo/addons/image_tag_environment b/setup/image_tag_environment/odoo/addons/image_tag_environment
new file mode 120000
index 0000000000..fdb0826de9
--- /dev/null
+++ b/setup/image_tag_environment/odoo/addons/image_tag_environment
@@ -0,0 +1 @@
+../../../../image_tag_environment
\ No newline at end of file
diff --git a/setup/image_tag_environment/setup.py b/setup/image_tag_environment/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/image_tag_environment/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/setup/storage_backend_ftp/odoo/addons/storage_backend_ftp b/setup/storage_backend_ftp/odoo/addons/storage_backend_ftp
new file mode 120000
index 0000000000..18973453c2
--- /dev/null
+++ b/setup/storage_backend_ftp/odoo/addons/storage_backend_ftp
@@ -0,0 +1 @@
+../../../../storage_backend_ftp
\ No newline at end of file
diff --git a/setup/storage_backend_ftp/setup.py b/setup/storage_backend_ftp/setup.py
new file mode 100644
index 0000000000..28c57bb640
--- /dev/null
+++ b/setup/storage_backend_ftp/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)
diff --git a/storage_backend_ftp/README.rst b/storage_backend_ftp/README.rst
new file mode 100644
index 0000000000..6e53cdb747
--- /dev/null
+++ b/storage_backend_ftp/README.rst
@@ -0,0 +1,81 @@
+.. image:: https://odoo-community.org/readme-banner-image
+ :target: https://odoo-community.org/get-involved?utm_source=readme
+ :alt: Odoo Community Association
+
+===================
+Storage Backend FTP
+===================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:56d23d879b05c109e87056403e4153adf478bf0ac3270e865cf1d248cfc6f3c5
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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/storage_backend_ftp
+ :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_ftp
+ :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|
+
+Add FTP as storage backend
+
+**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
+~~~~~~~
+
+* Acsone SA/NV
+
+Contributors
+~~~~~~~~~~~~
+
+* François Honoré
+* Lois Rilo
+
+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/storage_backend_ftp/__init__.py b/storage_backend_ftp/__init__.py
new file mode 100644
index 0000000000..0f00a6730d
--- /dev/null
+++ b/storage_backend_ftp/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import components
diff --git a/storage_backend_ftp/__manifest__.py b/storage_backend_ftp/__manifest__.py
new file mode 100644
index 0000000000..786f36a0c3
--- /dev/null
+++ b/storage_backend_ftp/__manifest__.py
@@ -0,0 +1,14 @@
+# Copyright 2021 ACSONE SA/NV ()
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
+{
+ "name": "Storage Backend FTP",
+ "summary": "Implement FTP Storage",
+ "version": "16.0.1.0.0",
+ "category": "Storage",
+ "website": "https://github.com/OCA/storage",
+ "author": " Acsone SA/NV,Odoo Community Association (OCA)",
+ "license": "LGPL-3",
+ "external_dependencies": {"python": ["pyftpdlib"]},
+ "depends": ["storage_backend"],
+ "data": ["views/backend_storage_view.xml"],
+}
diff --git a/storage_backend_ftp/components/__init__.py b/storage_backend_ftp/components/__init__.py
new file mode 100644
index 0000000000..72dbc3a308
--- /dev/null
+++ b/storage_backend_ftp/components/__init__.py
@@ -0,0 +1 @@
+from . import ftp_adapter
diff --git a/storage_backend_ftp/components/ftp_adapter.py b/storage_backend_ftp/components/ftp_adapter.py
new file mode 100644
index 0000000000..6f2d30a72b
--- /dev/null
+++ b/storage_backend_ftp/components/ftp_adapter.py
@@ -0,0 +1,168 @@
+# Copyright 2021 ACSONE SA/NV ()
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
+import errno
+import io
+import logging
+import os
+import ssl
+from contextlib import contextmanager
+from io import BytesIO
+
+from odoo.exceptions import UserError
+
+from odoo.addons.component.core import Component
+
+_logger = logging.getLogger(__name__)
+
+try:
+ import ftplib
+except ImportError as err: # pragma: no cover
+ _logger.debug(err)
+
+FTP_SECURITY_TO_PROTOCOL = {
+ "tls": ssl.PROTOCOL_TLS,
+ "tlsv1": ssl.PROTOCOL_TLSv1,
+ "tlsv1_1": ssl.PROTOCOL_TLSv1_1,
+ "tlsv1_2": ssl.PROTOCOL_TLSv1_2,
+ "sslv2": "sslv2 has been deprecated due to security issues",
+ "sslv23": ssl.PROTOCOL_SSLv23,
+ "sslv3": "sslv3 has been deprecated due to security issues",
+}
+
+
+def ftp_mkdirs(client, path):
+ try:
+ client.mkd(path)
+ except IOError as e:
+ if e.errno == errno.ENOENT and path:
+ ftp_mkdirs(client, os.path.dirname(path))
+ client.mkd(path)
+ else:
+ raise # pragma: no cover
+
+
+class ImplicitFTPTLS(ftplib.FTP_TLS):
+ """FTP_TLS subclass that automatically wraps sockets in SSL to support implicit FTPS."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._sock = None
+
+ @property
+ def sock(self):
+ """Return the socket."""
+ return self._sock
+
+ @sock.setter
+ def sock(self, value):
+ """When modifying the socket, ensure that it is ssl wrapped."""
+ if value is not None and not isinstance(value, ssl.SSLSocket):
+ value = self.context.wrap_socket(value)
+ self._sock = value
+
+
+@contextmanager
+def ftp(backend):
+ security = None
+ prot_p = False
+ if backend.ftp_encryption in ["ftp", "tls", "tls_explicit"]:
+ if backend.ftp_encryption == "ftp":
+ _ftp = ftplib.FTP(timeout=30)
+ elif backend.ftp_encryption == "tls":
+ _ftp = ImplicitFTPTLS()
+ # Due to a bug into between ftplib and ssl, this part (about ssl) might not work!
+ # https://bugs.python.org/issue31727
+ security = FTP_SECURITY_TO_PROTOCOL.get(backend.ftp_security, None)
+ prot_p = True
+ if isinstance(security, str):
+ raise UserError(security)
+ elif backend.ftp_encryption == "tls_explicit":
+ _ftp = ftplib.FTP_TLS()
+ prot_p = True
+ with _ftp as client:
+ if security:
+ client.ssl_version = security
+ client.connect(host=backend.ftp_server, port=backend.ftp_port)
+ client.login(backend.ftp_login, backend.ftp_password)
+ if prot_p:
+ client.prot_p()
+ if backend.ftp_passive:
+ client.set_pasv(True)
+ yield client
+
+
+class FTPStorageBackendAdapter(Component):
+ _name = "ftp.adapter"
+ _inherit = "base.storage.adapter"
+ _usage = "ftp"
+
+ def add(self, relative_path, data, **kwargs):
+ with ftp(self.collection) as client:
+ full_path = self._fullpath(relative_path)
+ dirname = os.path.dirname(full_path)
+ if dirname:
+ try:
+ client.cwd(dirname)
+ except IOError as e:
+ if e.errno == errno.ENOENT:
+ ftp_mkdirs(client, dirname)
+ else:
+ raise # pragma: no cover
+ with io.BytesIO(data) as tmp_file:
+ try:
+ client.storbinary("STOR " + full_path, tmp_file)
+ except ftplib.Error as e:
+ raise ValueError(repr(e)) from e
+ except OSError as e:
+ raise ValueError(repr(e)) from e
+
+ def get(self, relative_path, **kwargs):
+ full_path = self._fullpath(relative_path)
+ with ftp(self.collection) as client, BytesIO() as buff:
+ try:
+ client.retrbinary("RETR " + full_path, buff.write)
+ data = buff.getvalue()
+ except ftplib.Error as e:
+ raise FileNotFoundError(repr(e)) from e
+ return data
+
+ def list(self, relative_path):
+ full_path = self._fullpath(relative_path)
+ with ftp(self.collection) as client:
+ try:
+ return client.nlst(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 ftp(self.collection) as client:
+ for ftp_file in files:
+ dest_file_path = os.path.join(
+ destination_path, os.path.basename(ftp_file)
+ )
+ # Remove existing file at the destination path (an error is raised
+ # otherwise)
+ result = []
+ try:
+ result = client.nlst(dest_file_path)
+ except ftplib.Error:
+ _logger.debug("destination %s is free", dest_file_path)
+ if result:
+ client.delete(dest_file_path)
+ # Move the file using absolute filepaths
+ client.rename(fp(ftp_file), fp(dest_file_path))
+
+ def delete(self, relative_path):
+ full_path = self._fullpath(relative_path)
+ with ftp(self.collection) as client:
+ return client.delete(full_path)
+
+ def validate_config(self):
+ with ftp(self.collection) as client:
+ client.getwelcome()
diff --git a/storage_backend_ftp/i18n/storage_backend_ftp.pot b/storage_backend_ftp/i18n/storage_backend_ftp.pot
new file mode 100644
index 0000000000..2f780a8c0b
--- /dev/null
+++ b/storage_backend_ftp/i18n/storage_backend_ftp.pot
@@ -0,0 +1,121 @@
+# Translation of Odoo Server.
+# This file contains the translation of the following modules:
+# * storage_backend_ftp
+#
+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_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__backend_type
+msgid "Backend Type"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__backend_type_env_default
+msgid "Backend Type Env Default"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__tls_explicit
+msgid "Explicit FTP over TLS"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__backend_type__ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__ftp
+#: model_terms:ir.ui.view,arch_db:storage_backend_ftp.storage_backend_view_form
+msgid "FTP"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_encryption
+msgid "FTP Encryption method"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_server
+msgid "FTP Host"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_login
+msgid "FTP Login"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_passive
+msgid "FTP Passive"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_password
+msgid "FTP Password"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_port
+msgid "FTP Port"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,field_description:storage_backend_ftp.field_storage_backend__ftp_security
+msgid "FTP security option"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_encryption__tls
+msgid "Implicit FTP over TLS"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields,help:storage_backend_ftp.field_storage_backend__ftp_login
+msgid "Login to connect to ftp server"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__none
+msgid "None"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv2
+msgid "SSLv2"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv23
+msgid "SSLv23"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__sslv3
+msgid "SSLv3"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model,name:storage_backend_ftp.model_storage_backend
+msgid "Storage Backend"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1
+msgid "TLS"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1_1
+msgid "TLSv1_1"
+msgstr ""
+
+#. module: storage_backend_ftp
+#: model:ir.model.fields.selection,name:storage_backend_ftp.selection__storage_backend__ftp_security__tlsv1_2
+msgid "TLSv1_2"
+msgstr ""
diff --git a/storage_backend_ftp/models/__init__.py b/storage_backend_ftp/models/__init__.py
new file mode 100644
index 0000000000..f45f402268
--- /dev/null
+++ b/storage_backend_ftp/models/__init__.py
@@ -0,0 +1 @@
+from . import storage_backend
diff --git a/storage_backend_ftp/models/storage_backend.py b/storage_backend_ftp/models/storage_backend.py
new file mode 100644
index 0000000000..6743911741
--- /dev/null
+++ b/storage_backend_ftp/models/storage_backend.py
@@ -0,0 +1,55 @@
+# Copyright 2021 ACSONE SA/NV ()
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
+from odoo import fields, models
+
+
+class StorageBackend(models.Model):
+ _inherit = "storage.backend"
+
+ backend_type = fields.Selection(
+ selection_add=[("ftp", "FTP")], ondelete={"ftp": "set default"}
+ )
+ ftp_server = fields.Char(string="FTP Host")
+ ftp_port = fields.Integer(string="FTP Port", default=21)
+ ftp_encryption = fields.Selection(
+ string="FTP Encryption method",
+ selection=[
+ ("ftp", "FTP"),
+ ("tls", "Implicit FTP over TLS"),
+ ("tls_explicit", "Explicit FTP over TLS"),
+ ],
+ default="ftp",
+ required=True,
+ )
+ ftp_security = fields.Selection(
+ string="FTP security option",
+ selection=[
+ ("none", "None"),
+ ("tlsv1", "TLS"),
+ ("tlsv1_1", "TLSv1_1"),
+ ("tlsv1_2", "TLSv1_2"),
+ ("sslv2", "SSLv2"),
+ ("sslv23", "SSLv23"),
+ ("sslv3", "SSLv3"),
+ ],
+ required=True,
+ )
+ ftp_login = fields.Char(string="FTP Login", help="Login to connect to ftp server")
+ ftp_password = fields.Char(string="FTP Password")
+ ftp_passive = fields.Boolean(string="FTP Passive", default=False)
+
+ @property
+ def _server_env_fields(self):
+ env_fields = super()._server_env_fields
+ env_fields.update(
+ {
+ "ftp_password": {},
+ "ftp_login": {},
+ "ftp_server": {},
+ "ftp_port": {},
+ "ftp_encryption": {},
+ "ftp_security": {},
+ "ftp_passive": {},
+ }
+ )
+ return env_fields
diff --git a/storage_backend_ftp/readme/CONTRIBUTORS.rst b/storage_backend_ftp/readme/CONTRIBUTORS.rst
new file mode 100644
index 0000000000..51b8da248c
--- /dev/null
+++ b/storage_backend_ftp/readme/CONTRIBUTORS.rst
@@ -0,0 +1,2 @@
+* François Honoré
+* Lois Rilo
diff --git a/storage_backend_ftp/readme/DESCRIPTION.rst b/storage_backend_ftp/readme/DESCRIPTION.rst
new file mode 100644
index 0000000000..b0b32036fc
--- /dev/null
+++ b/storage_backend_ftp/readme/DESCRIPTION.rst
@@ -0,0 +1 @@
+Add FTP as storage backend
diff --git a/storage_backend_ftp/static/description/icon.png b/storage_backend_ftp/static/description/icon.png
new file mode 100644
index 0000000000..3a0328b516
Binary files /dev/null and b/storage_backend_ftp/static/description/icon.png differ
diff --git a/storage_backend_ftp/static/description/index.html b/storage_backend_ftp/static/description/index.html
new file mode 100644
index 0000000000..5c6201754a
--- /dev/null
+++ b/storage_backend_ftp/static/description/index.html
@@ -0,0 +1,430 @@
+
+
+
+
+
+README.rst
+
+
+
+
+
+
+
+
+
+
+
Storage Backend FTP
+
+

+
Add FTP as storage backend
+
Table of contents
+
+
+
+
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.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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/storage_backend_ftp/tests/__init__.py b/storage_backend_ftp/tests/__init__.py
new file mode 100644
index 0000000000..9ab6aa0f49
--- /dev/null
+++ b/storage_backend_ftp/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_ftp
diff --git a/storage_backend_ftp/tests/test_ftp.py b/storage_backend_ftp/tests/test_ftp.py
new file mode 100644
index 0000000000..620511cf31
--- /dev/null
+++ b/storage_backend_ftp/tests/test_ftp.py
@@ -0,0 +1,162 @@
+# Copyright 2021 ACSONE SA/NV ()
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
+
+# pylint: disable=missing-manifest-dependency
+# disable warning on 'vcr' missing in manifest: this is only a dependency for
+# dev/tests
+
+import errno
+import ftplib
+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_ftp.components.ftp_adapter"
+ADAPTER_PATH = MOD_PATH + ".FTPStorageBackendAdapter"
+FTP_LIB_PATH = MOD_PATH + ".ftplib"
+
+
+class FtpCase(CommonCase, BackendStorageTestMixin):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.backend.write(
+ {
+ "backend_type": "ftp",
+ "ftp_login": os.environ.get("FTP_LOGIN", "foo"),
+ "ftp_password": os.environ.get("FTP_PWD", "pass"),
+ "ftp_server": os.environ.get("FTP_HOST", "localhost"),
+ "ftp_port": os.environ.get("FTP_PORT", "21"),
+ "directory_path": "upload",
+ "ftp_encryption": "ftp",
+ }
+ )
+ cls.case_with_subdirectory = "upload/subdirectory/here"
+
+ @mock.patch(MOD_PATH + ".ftp_mkdirs")
+ @mock.patch(FTP_LIB_PATH)
+ def test_add(self, mocked_ftplib, mocked_mkdirs):
+ client = mocked_ftplib.FTP().__enter__()
+ # simulate errors
+ exc = IOError()
+ # general
+ client.cwd.side_effect = exc
+ with self.assertRaises(IOError):
+ self.backend.add("fake/path", b"fake data")
+ # not found
+ exc.errno = errno.ENOENT
+ client.cwd.side_effect = exc
+ file_data = b"fake data"
+ with mock.patch("io.BytesIO") as tmp_file:
+ self.backend.add("fake/path", file_data)
+ # mkdirs has been called
+ mocked_mkdirs.assert_called()
+ client.storbinary.assert_called()
+ tmp_file.assert_called()
+ tmp_file.assert_called_with(file_data)
+ client.storbinary.assert_called_with(
+ "STOR upload/fake/path", tmp_file().__enter__()
+ )
+
+ @mock.patch(FTP_LIB_PATH)
+ def test_get(self, mocked_ftplib):
+ client = mocked_ftplib.FTP().__enter__()
+ content = b"filecontent"
+ with open("/tmp/fakefile2.txt", "w+b") as fakefile:
+ fakefile.write(content)
+
+ def side_effect_retrbinary(*args, **kwargs):
+ """
+ Mock function to read tmp file.
+ """
+ cmd, buff_write = args
+ with open("/tmp/fakefile2.txt", "rb") as tmp_file:
+ buff_write(tmp_file.read())
+
+ client.retrbinary.side_effect = side_effect_retrbinary
+ self.assertEqual(self.backend.get("fake/path"), content)
+
+ @mock.patch(FTP_LIB_PATH)
+ def test_list(self, mocked_ftplib):
+ client = mocked_ftplib.FTP().__enter__()
+ # simulate errors
+ exc = IOError()
+ # general
+ client.nlst.side_effect = exc
+ with self.assertRaises(IOError):
+ self.backend.list_files()
+ # not found
+ exc.errno = errno.ENOENT
+ client.nlst.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
+ )
+
+ # Do not patch the entire ftplib otherwise the error_perm Exception
+ # become also a mock and then a traceback is generated on the "except ftplib.error_perm"
+ # because this ftplib.error_perm is not really an Exception (but a mock)!
+ @mock.patch(FTP_LIB_PATH + ".FTP")
+ def test_move_files(self, mocked_ftplib):
+ client = mocked_ftplib().__enter__()
+ # simulate file is not already there
+ client.nlst.side_effect = ftplib.error_perm()
+ 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.delete.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.nlst.side_effect = None
+ client.nlst.return_value = True
+ self.backend.move_files([to_move], to_path)
+ # client will delete it first
+ client.delete.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")
+ )
+
+ @mock.patch(FTP_LIB_PATH)
+ def test_delete(self, mocked_ftplib):
+ client = mocked_ftplib.FTP().__enter__()
+ path = "delete/a/path"
+ self.backend.delete(path)
+ client.delete.assert_called_with(
+ os.path.join(self.backend.directory_path, path)
+ )
+
+ @mock.patch(FTP_LIB_PATH)
+ def test_validate_config(self, mocked_ftplib):
+ client = mocked_ftplib.FTP().__enter__()
+ self.backend.action_test_config()
+ client.getwelcome.assert_called()
+
+ @mock.patch(FTP_LIB_PATH)
+ def test_mkd(self, mocked_ftplib):
+ client = mocked_ftplib.FTP().__enter__()
+ # simulate errors
+ exc = IOError()
+ exc.errno = errno.ENOENT
+ # general
+ client.cwd.side_effect = exc
+ client.mkd.side_effect = exc
+ with self.assertRaises(OSError):
+ self.backend.add("fake/path", b"fake data")
+ client.mkd.assert_called()
diff --git a/storage_backend_ftp/views/backend_storage_view.xml b/storage_backend_ftp/views/backend_storage_view.xml
new file mode 100644
index 0000000000..b20d0f87be
--- /dev/null
+++ b/storage_backend_ftp/views/backend_storage_view.xml
@@ -0,0 +1,28 @@
+
+
+
+ storage.backend
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+