diff --git a/fs_attachment/migrations/16.0.3.0.0/post-migration.py b/fs_attachment/migrations/16.0.3.0.0/post-migration.py deleted file mode 100644 index 6d3ab374a9..0000000000 --- a/fs_attachment/migrations/16.0.3.0.0/post-migration.py +++ /dev/null @@ -1,16 +0,0 @@ -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - if env["ir.module.module"].search( - [("name", "=", "server_environment"), ("state", "=", "installed")] - ): - openupgrade.logged_query( - env.cr, - """ - UPDATE ir_module_module - SET state = 'to install' - WHERE name = 'fs_attachment_environment' AND state = 'uninstalled' - """, - ) diff --git a/fs_attachment/upgrades/16.0.3.0.0/post-update.py b/fs_attachment/upgrades/16.0.3.0.0/post-update.py new file mode 100644 index 0000000000..e6627838d5 --- /dev/null +++ b/fs_attachment/upgrades/16.0.3.0.0/post-update.py @@ -0,0 +1,21 @@ +from openupgradelib import openupgrade + +from odoo import _, exceptions + + +@openupgrade.migrate() +def migrate(env, version): + module = env["ir.module.module"].search( + [("name", "=", "fs_attachment_environment")] + ) + if not module: + raise exceptions.UserError( + _( + "The 'fs_attachment_environment' module is not available. " + "It is required to preserve the server environment managed " + "fields of 'fs.storage'. Make it available on the " + "addons path before upgrading 'fs_attachment'." + ) + ) + if module.state == "uninstalled": + module.button_install() diff --git a/fs_attachment_environment/__init__.py b/fs_attachment_environment/__init__.py index 0650744f6b..1a9a001cf7 100644 --- a/fs_attachment_environment/__init__.py +++ b/fs_attachment_environment/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/fs_attachment_environment/__manifest__.py b/fs_attachment_environment/__manifest__.py index 8e4f8ae33a..bd352a0874 100644 --- a/fs_attachment_environment/__manifest__.py +++ b/fs_attachment_environment/__manifest__.py @@ -14,4 +14,6 @@ "depends": ["fs_storage_environment", "fs_attachment"], "data": [], "auto_install": True, + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", } diff --git a/fs_attachment_environment/hooks.py b/fs_attachment_environment/hooks.py new file mode 100644 index 0000000000..d07bfa368f --- /dev/null +++ b/fs_attachment_environment/hooks.py @@ -0,0 +1,50 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from psycopg2 import sql + +from odoo import SUPERUSER_ID, api + +from odoo.addons.server_environment.uninstall import restore_env_managed_columns + +ENV_MANAGED_FIELDS = [ + "optimizes_directory_path", + "autovacuum_gc", + "base_url", + "is_directory_path_in_url", + "use_x_sendfile_to_serve_internal_url", + "use_as_default_for_attachments", + "force_db_for_default_attachment_rules", + "use_filename_obfuscation", + "model_xmlids", + "field_xmlids", +] + + +def post_init_hook(cr, registry): + """Preserve fallback values without violating the attachment rule constraint. + + On a fresh install, Odoo initializes the new force database rules column on + existing storages with its non-empty default, even when the storage is not + the default attachment storage. The preservation helper writes fields back + through the ORM one at a time, so this temporary inconsistent state would + trigger the constraint. Normalize the stored fallback with SQL first. + """ + env = api.Environment(cr, SUPERUSER_ID, {}) + env.cr.execute( + sql.SQL("UPDATE {} SET {} = NULL WHERE NOT COALESCE({}, FALSE)").format( + sql.Identifier(env["fs.storage"]._table), + sql.Identifier("force_db_for_default_attachment_rules"), + sql.Identifier("use_as_default_for_attachments"), + ) + ) + env["fs.storage"]._preserve_not_env_managed_data(ENV_MANAGED_FIELDS) + + +def uninstall_hook(env): + """Restore database columns dropped by server.env.mixin.""" + restore_env_managed_columns( + env, + "fs.storage", + ENV_MANAGED_FIELDS, + ) diff --git a/fs_attachment_s3/migrations/16.0.3.0.0/post-migration.py b/fs_attachment_s3/migrations/16.0.3.0.0/post-migration.py deleted file mode 100644 index dc879c01d4..0000000000 --- a/fs_attachment_s3/migrations/16.0.3.0.0/post-migration.py +++ /dev/null @@ -1,16 +0,0 @@ -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - if env["ir.module.module"].search( - [("name", "=", "server_environment"), ("state", "=", "installed")] - ): - openupgrade.logged_query( - env.cr, - """ - UPDATE ir_module_module - SET state = 'to install' - WHERE name = 'fs_attachment_s3_environment' AND state = 'uninstalled' - """, - ) diff --git a/fs_attachment_s3/upgrades/16.0.3.0.0/post-update.py b/fs_attachment_s3/upgrades/16.0.3.0.0/post-update.py new file mode 100644 index 0000000000..19299f15da --- /dev/null +++ b/fs_attachment_s3/upgrades/16.0.3.0.0/post-update.py @@ -0,0 +1,21 @@ +from openupgradelib import openupgrade + +from odoo import _, exceptions + + +@openupgrade.migrate() +def migrate(env, version): + module = env["ir.module.module"].search( + [("name", "=", "fs_attachment_s3_environment")] + ) + if not module: + raise exceptions.UserError( + _( + "The 'fs_attachment_s3_environment' module is not available. " + "It is required to preserve the server environment managed " + "fields of 'fs.storage'. Make it available on the " + "addons path before upgrading 'fs_attachment_s3'." + ) + ) + if module.state == "uninstalled": + module.button_install() diff --git a/fs_attachment_s3_environment/__init__.py b/fs_attachment_s3_environment/__init__.py index 0650744f6b..1a9a001cf7 100644 --- a/fs_attachment_s3_environment/__init__.py +++ b/fs_attachment_s3_environment/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/fs_attachment_s3_environment/__manifest__.py b/fs_attachment_s3_environment/__manifest__.py index 9f8a8639b5..51c1638250 100644 --- a/fs_attachment_s3_environment/__manifest__.py +++ b/fs_attachment_s3_environment/__manifest__.py @@ -14,4 +14,6 @@ "depends": ["fs_attachment_environment", "fs_attachment_s3"], "data": [], "auto_install": True, + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", } diff --git a/fs_attachment_s3_environment/hooks.py b/fs_attachment_s3_environment/hooks.py new file mode 100644 index 0000000000..c18fccd9e7 --- /dev/null +++ b/fs_attachment_s3_environment/hooks.py @@ -0,0 +1,25 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import SUPERUSER_ID, api + +from odoo.addons.server_environment.uninstall import restore_env_managed_columns + +ENV_MANAGED_FIELDS = [ + "s3_uses_signed_url_for_x_sendfile", + "s3_signed_url_expiration", +] + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["fs.storage"]._preserve_not_env_managed_data(ENV_MANAGED_FIELDS) + + +def uninstall_hook(env): + """Restore database columns dropped by server.env.mixin.""" + restore_env_managed_columns( + env, + "fs.storage", + ENV_MANAGED_FIELDS, + ) diff --git a/fs_storage/migrations/16.0.2.0.0/post-migration.py b/fs_storage/migrations/16.0.2.0.0/post-migration.py deleted file mode 100644 index 187a8b0c74..0000000000 --- a/fs_storage/migrations/16.0.2.0.0/post-migration.py +++ /dev/null @@ -1,16 +0,0 @@ -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - if env["ir.module.module"].search( - [("name", "=", "server_environment"), ("state", "=", "installed")] - ): - openupgrade.logged_query( - env.cr, - """ - UPDATE ir_module_module - SET state = 'to install' - WHERE name = 'fs_storage_environment' AND state = 'uninstalled' - """, - ) diff --git a/fs_storage/upgrades/16.0.2.0.0/post-update.py b/fs_storage/upgrades/16.0.2.0.0/post-update.py new file mode 100644 index 0000000000..626ada9ca6 --- /dev/null +++ b/fs_storage/upgrades/16.0.2.0.0/post-update.py @@ -0,0 +1,19 @@ +from openupgradelib import openupgrade + +from odoo import _, exceptions + + +@openupgrade.migrate() +def migrate(env, version): + module = env["ir.module.module"].search([("name", "=", "fs_storage_environment")]) + if not module: + raise exceptions.UserError( + _( + "The 'fs_storage_environment' module is not available. " + "It is required to preserve the server environment managed " + "fields of 'fs.storage'. Make it available on the " + "addons path before upgrading 'fs_storage'." + ) + ) + if module.state == "uninstalled": + module.button_install() diff --git a/fs_storage_backup/migrations/16.0.2.0.0/post-migration.py b/fs_storage_backup/migrations/16.0.2.0.0/post-migration.py deleted file mode 100644 index 50b8d1c896..0000000000 --- a/fs_storage_backup/migrations/16.0.2.0.0/post-migration.py +++ /dev/null @@ -1,16 +0,0 @@ -from openupgradelib import openupgrade - - -@openupgrade.migrate() -def migrate(env, version): - if env["ir.module.module"].search( - [("name", "=", "server_environment"), ("state", "=", "installed")] - ): - openupgrade.logged_query( - env.cr, - """ - UPDATE ir_module_module - SET state = 'to install' - WHERE name = 'fs_storage_backup_environment' AND state = 'uninstalled' - """, - ) diff --git a/fs_storage_backup/upgrades/16.0.2.0.0/post-update.py b/fs_storage_backup/upgrades/16.0.2.0.0/post-update.py new file mode 100644 index 0000000000..dac91a2118 --- /dev/null +++ b/fs_storage_backup/upgrades/16.0.2.0.0/post-update.py @@ -0,0 +1,21 @@ +from openupgradelib import openupgrade + +from odoo import _, exceptions + + +@openupgrade.migrate() +def migrate(env, version): + module = env["ir.module.module"].search( + [("name", "=", "fs_storage_backup_environment")] + ) + if not module: + raise exceptions.UserError( + _( + "The 'fs_storage_backup_environment' module is not available. " + "It is required to preserve the server environment managed " + "fields of 'fs.storage'. Make it available on the " + "addons path before upgrading 'fs_storage_backup'." + ) + ) + if module.state == "uninstalled": + module.button_install() diff --git a/fs_storage_backup_environment/__init__.py b/fs_storage_backup_environment/__init__.py index 0650744f6b..1a9a001cf7 100644 --- a/fs_storage_backup_environment/__init__.py +++ b/fs_storage_backup_environment/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/fs_storage_backup_environment/__manifest__.py b/fs_storage_backup_environment/__manifest__.py index 9b4e4b242e..2f315afdd7 100644 --- a/fs_storage_backup_environment/__manifest__.py +++ b/fs_storage_backup_environment/__manifest__.py @@ -14,4 +14,6 @@ "depends": ["fs_storage_environment", "fs_storage_backup"], "data": [], "auto_install": True, + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", } diff --git a/fs_storage_backup_environment/hooks.py b/fs_storage_backup_environment/hooks.py new file mode 100644 index 0000000000..111796c7ad --- /dev/null +++ b/fs_storage_backup_environment/hooks.py @@ -0,0 +1,28 @@ +# Copyright 2026 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import SUPERUSER_ID, api + +from odoo.addons.server_environment.uninstall import restore_env_managed_columns + +ENV_MANAGED_FIELDS = [ + "use_for_backup", + "backup_include_filestore", + "backup_filename_format", + "backup_keep_time", + "backup_dir", +] + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["fs.storage"]._preserve_not_env_managed_data(ENV_MANAGED_FIELDS) + + +def uninstall_hook(env): + """Restore database columns dropped by server.env.mixin.""" + restore_env_managed_columns( + env, + "fs.storage", + ENV_MANAGED_FIELDS, + ) diff --git a/fs_storage_environment/__init__.py b/fs_storage_environment/__init__.py index 0650744f6b..1a9a001cf7 100644 --- a/fs_storage_environment/__init__.py +++ b/fs_storage_environment/__init__.py @@ -1 +1,2 @@ from . import models +from .hooks import post_init_hook, uninstall_hook diff --git a/fs_storage_environment/__manifest__.py b/fs_storage_environment/__manifest__.py index 9c665f6e1a..d686cf8d92 100644 --- a/fs_storage_environment/__manifest__.py +++ b/fs_storage_environment/__manifest__.py @@ -13,4 +13,6 @@ "installable": True, "depends": ["fs_storage", "server_environment"], "data": [], + "post_init_hook": "post_init_hook", + "uninstall_hook": "uninstall_hook", } diff --git a/fs_storage_environment/hooks.py b/fs_storage_environment/hooks.py new file mode 100644 index 0000000000..7812e910f1 --- /dev/null +++ b/fs_storage_environment/hooks.py @@ -0,0 +1,29 @@ +# Copyright 2026 Camptocamp SA +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import SUPERUSER_ID, api + +from odoo.addons.server_environment.uninstall import restore_env_managed_columns + +ENV_MANAGED_FIELDS = [ + "protocol", + "options", + "directory_path", + "eval_options_from_env", + "check_connection_method", +] + + +def post_init_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + env["fs.storage"]._preserve_not_env_managed_data(ENV_MANAGED_FIELDS) + + +def uninstall_hook(env): + """Restore database columns dropped by server.env.mixin.""" + restore_env_managed_columns( + env, + "fs.storage", + ENV_MANAGED_FIELDS, + field_defaults={"protocol": "odoofs"}, + )