diff --git a/README.md b/README.md index db094f8a9..b16c4178f 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Available addons addon | version | maintainers | summary --- | --- | --- | --- [data_encryption](data_encryption/) | 16.0.1.0.1 | | Store accounts and credentials encrypted by environment -[mail_environment](mail_environment/) | 16.0.1.0.2 | | Configure mail servers with server_environment_files +[mail_environment](mail_environment/) | 16.0.1.0.3 | | Configure mail servers with server_environment_files [mail_environment_google_gmail](mail_environment_google_gmail/) | 16.0.1.1.0 | ivantodorovich | Configure Gmail mail servers with server_environment_files [pos_environment](pos_environment/) | 16.0.1.0.1 | legalsylvain | Custom messages on the bill depending on the environment -[server_environment](server_environment/) | 16.0.1.1.2 | | move some configurations out of the database +[server_environment](server_environment/) | 16.0.1.1.3 | | move some configurations out of the database [server_environment_data_encryption](server_environment_data_encryption/) | 16.0.1.0.0 | | Server Environment Data Encryption [server_environment_ir_config_parameter](server_environment_ir_config_parameter/) | 16.0.1.1.0 | | Override System Parameters from server environment file diff --git a/mail_environment/README.rst b/mail_environment/README.rst index 2752de28f..0943a5823 100644 --- a/mail_environment/README.rst +++ b/mail_environment/README.rst @@ -11,7 +11,7 @@ Mail configuration with server_environment !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:13d1095e576fa39cc25f6ef96af154d00548c8a900ebf5f73281527ce63a0d05 + !! source digest: sha256:1f26c6991b2a9a73300042b6bf8c714d8a5f0f22484a4226b9bd4131406e3291 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/mail_environment/__manifest__.py b/mail_environment/__manifest__.py index 17c51f15f..3a09b0012 100644 --- a/mail_environment/__manifest__.py +++ b/mail_environment/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Mail configuration with server_environment", - "version": "16.0.1.0.2", + "version": "16.0.1.0.3", "category": "Tools", "summary": "Configure mail servers with server_environment_files", "author": "Camptocamp, Odoo Community Association (OCA)", diff --git a/mail_environment/migrations/16.0.1.0.3/post-migration.py b/mail_environment/migrations/16.0.1.0.3/post-migration.py new file mode 100644 index 000000000..523be970a --- /dev/null +++ b/mail_environment/migrations/16.0.1.0.3/post-migration.py @@ -0,0 +1,9 @@ +# Copyright 2026 ACSONE SA/NV +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +@openupgrade.migrate() +def migrate(env, version): + env["ir.mail_server"]._preserve_not_env_managed_data(["smtp_authentication"]) diff --git a/mail_environment/static/description/index.html b/mail_environment/static/description/index.html index 76793343e..82d5299de 100644 --- a/mail_environment/static/description/index.html +++ b/mail_environment/static/description/index.html @@ -372,7 +372,7 @@

Mail configuration with server_environment

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:13d1095e576fa39cc25f6ef96af154d00548c8a900ebf5f73281527ce63a0d05 +!! source digest: sha256:1f26c6991b2a9a73300042b6bf8c714d8a5f0f22484a4226b9bd4131406e3291 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module allows to configure the incoming and outgoing mail servers diff --git a/server_environment/README.rst b/server_environment/README.rst index 6cbcc3853..7b2092045 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -11,7 +11,7 @@ server configuration environment files !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:5062bdb51631430aaf2892a874377786dfe3f0fc40b0e058f744cca1f7ffeaaa + !! source digest: sha256:ac1eeedab3b4cf31ddd2d67fab4968abd860e8bb7b53d28d9d7f4fd30cc6dcd6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png @@ -203,6 +203,161 @@ If you want to have a technical name to reference:: [...] +Restoring columns on uninstall +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``server.env.mixin`` is bound to an existing model, the ORM drops the +original stored columns for all env-managed fields. If the binding addon is +later uninstalled, those columns must be recreated so the database remains +usable. + +Add an ``uninstall_hook`` to your addon and delegate to +``restore_env_managed_columns``:: + + # your_addon/__init__.py + from .hooks import uninstall_hook + # your_addon/hooks.py + from odoo import SUPERUSER_ID, api + from odoo.addons.server_environment import uninstall + + def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + uninstall.restore_env_managed_columns( + env, + "storage.backend", + ["directory_path", "other_field"], + ) + + # your_addon/__manifest__.py + { + ... + "uninstall_hook": "uninstall_hook", + } + +The helper creates any missing columns (idempotent: safe to call multiple +times) and repopulates them with each record's current effective value — +whether that value came from an environment configuration file or from the +stored default field (``x__env_default``). + +The hook must run *before* the ORM extensions are removed, which is guaranteed +by Odoo's uninstall sequence (hooks execute before +``Module.module_uninstall()``). + +Handling required fields +^^^^^^^^^^^^^^^^^^^^^^^^ + +If a restored column is **required** (has a ``NOT NULL`` constraint) but has no +effective value (missing from environment config and no default field set), the +restoration will fail with a ``UserError``. + +**Solution:** pass a ``field_defaults`` dictionary with fallback values:: + + def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + restore_env_managed_columns( + env, + "ir.mail_server", + ["smtp_host", "smtp_authentication"], + field_defaults={ + "smtp_authentication": "login", # fallback for required field + }, + ) + +The helper will use the fallback value if provided and the computed field value +is empty. If no fallback is provided but a required field has no value, a +``UserError`` is raised with instructions on how to provide a ``field_defaults`` +parameter. + +Migrating when dropping server_environment dependency +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When refactoring an existing addon that embeds a ``server.env.mixin`` binding, you +may want to extract the binding into a separate *glue* addon and drop the +``server_environment`` dependency from the original. This keeps the base addon +lightweight while preserving server-environment features for those who install +the glue addon. + +**Pattern:** + +- **Original addon (v1)**: depends on ``server_environment`` and binds the mixin + directly in model code. +- **Refactored addon (v2)**: removes ``server_environment`` from dependencies, + removes the mixin binding and the related ORM model inheritance. +- **New glue addon** (optional, same version): depends on both + ``server_environment`` and the original addon v2; re-adds the mixin binding in + a separate module file. + +**Migration checklist:** + +1. In the **original addon's v2** ``__manifest__.py``: + + - Remove ``"server_environment"`` from ``depends``. + - Remove the model file(s) that contained the mixin binding. + - Update ``depends`` to add the new glue addon *if* the base addon still needs + it (otherwise, make the glue addon optional for users who want env-binding). + +2. In the **original addon's v2 model code**: + + - Delete or simplify the model class that inherited from + ``server.env.mixin``. + - If the model was only there for the binding, remove it entirely. + - Restore the original field definitions (not as computed fields). + +3. **Create a migration script** (if needed) to restore columns *during the addon + upgrade*, before the ORM model extensions are unloaded. Use a ``@post_load`` + hook or a dedicated migration script:: + + # migrations/18.0.1.0.0/post-restore-columns.py + def migrate(cr, version): + # Call the restoration logic while the v1 model is still active + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + # If any field is required and may have no value in the environment, + # provide a fallback via field_defaults + restore_env_managed_columns( + env, + "storage.backend", + ["directory_path", "other_field"], + field_defaults={ + "directory_path": "/tmp", # fallback for required field + }, + ) + +4. **Create the glue addon** with the model re-inheritance:: + + # your_addon_env/__init__.py + from . import models + + # your_addon_env/models/__init__.py + from . import storage_backend + + # your_addon_env/models/storage_backend.py + class StorageBackend(models.Model): + _name = "storage.backend" + _inherit = ["storage.backend", "server.env.mixin"] + + @property + def _server_env_fields(self): + return {"directory_path": {}} + + # your_addon_env/__manifest__.py + { + "name": "Storage Backend – Server Environment", + "version": "18.0.1.0.0", + "depends": ["server_environment", "storage_backend"], + "installable": True, + } + +**Key points:** + +- Column restoration must happen *during the addon upgrade* (step 3), not as an + uninstall hook, because the original model binding is still active. +- The ``restore_env_managed_columns`` helper is idempotent and safe to call even + if columns already exist. +- Users who do not need server environment features simply do *not* install the + glue addon—the base addon continues to work with plain database columns. +- Users who do need server environment can install both the base addon (v2+) and + the glue addon (same version) to get the binding back. + Known issues / Roadmap ====================== diff --git a/server_environment/__init__.py b/server_environment/__init__.py index 52bc6e602..85c79f6fd 100644 --- a/server_environment/__init__.py +++ b/server_environment/__init__.py @@ -1,3 +1,4 @@ from . import models from . import server_env from .server_env import serv_config, setboolean +from . import uninstall diff --git a/server_environment/__manifest__.py b/server_environment/__manifest__.py index c108cd429..23b5fccf1 100644 --- a/server_environment/__manifest__.py +++ b/server_environment/__manifest__.py @@ -4,7 +4,7 @@ { "name": "server configuration environment files", - "version": "16.0.1.1.2", + "version": "16.0.1.1.3", "depends": ["base", "base_sparse_field"], "author": "Camptocamp,Odoo Community Association (OCA)", "summary": "move some configurations out of the database", diff --git a/server_environment/i18n/it.po b/server_environment/i18n/it.po index b417622af..f50bd3ee8 100644 --- a/server_environment/i18n/it.po +++ b/server_environment/i18n/it.po @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: server-tools (9.0)\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-09-10 02:53+0000\n" -"PO-Revision-Date: 2025-05-16 09:20+0000\n" +"PO-Revision-Date: 2026-08-24 08:46+0000\n" "Last-Translator: mymage \n" "Language-Team: Italian (http://www.transifex.com/oca/OCA-server-tools-9-0/" "language/it/)\n" @@ -17,7 +17,7 @@ msgstr "" "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" +"X-Generator: Weblate 5.15.2\n" #. module: server_environment #: model:ir.model.fields,field_description:server_environment.field_server_config__config @@ -279,7 +279,7 @@ msgstr "odoo | list_db" #. module: server_environment #: model:ir.model.fields,field_description:server_environment.field_server_config__odoo_I_log_config msgid "odoo | log_config" -msgstr "" +msgstr "odoo | log_config" #. module: server_environment #: model:ir.model.fields,field_description:server_environment.field_server_config__odoo_I_log_db diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index f5cfa1c8c..e45052829 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -425,3 +425,31 @@ def _setup_base(self): self._server_env_transform_field_to_read_from_env(field) self._server_env_add_is_editable_field(field) return + + @api.model + def _preserve_not_env_managed_data(self, field_name_list): + """ + Helper function typically used for hooks and migration scripts. + Restores database values for fields transitioning to 'server env managed'. + + When a field is defined as managed by the server environment, Odoo + ignores the value stored in the database, prioritizing the environment + configuration instead. If no environment configuration exists, the field + may effectively lose its previous value. + + This method forces to 'persist' these values if they are not + explicitly overridden by the current environment configuration. + """ + self.env.cr.execute(f"SELECT * FROM {self._table}") + for row in self.env.cr.dictfetchall(): + record = ( + self.env[self._name] + .with_context(active_test=False) + .search([("id", "=", row["id"])]) + ) + if record: + record_values = {} + for field_name in field_name_list: + if field_name in row: + record_values[field_name] = row[field_name] + record.update(record_values) diff --git a/server_environment/readme/USAGE.rst b/server_environment/readme/USAGE.rst index deeea866a..47736e0a6 100644 --- a/server_environment/readme/USAGE.rst +++ b/server_environment/readme/USAGE.rst @@ -22,3 +22,158 @@ If you want to have a technical name to reference:: _inherit = ["storage.backend", "server.env.techname.mixin"] [...] + +Restoring columns on uninstall +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When ``server.env.mixin`` is bound to an existing model, the ORM drops the +original stored columns for all env-managed fields. If the binding addon is +later uninstalled, those columns must be recreated so the database remains +usable. + +Add an ``uninstall_hook`` to your addon and delegate to +``restore_env_managed_columns``:: + + # your_addon/__init__.py + from .hooks import uninstall_hook + # your_addon/hooks.py + from odoo import SUPERUSER_ID, api + from odoo.addons.server_environment import uninstall + + def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + uninstall.restore_env_managed_columns( + env, + "storage.backend", + ["directory_path", "other_field"], + ) + + # your_addon/__manifest__.py + { + ... + "uninstall_hook": "uninstall_hook", + } + +The helper creates any missing columns (idempotent: safe to call multiple +times) and repopulates them with each record's current effective value — +whether that value came from an environment configuration file or from the +stored default field (``x__env_default``). + +The hook must run *before* the ORM extensions are removed, which is guaranteed +by Odoo's uninstall sequence (hooks execute before +``Module.module_uninstall()``). + +Handling required fields +^^^^^^^^^^^^^^^^^^^^^^^^ + +If a restored column is **required** (has a ``NOT NULL`` constraint) but has no +effective value (missing from environment config and no default field set), the +restoration will fail with a ``UserError``. + +**Solution:** pass a ``field_defaults`` dictionary with fallback values:: + + def uninstall_hook(cr, registry): + env = api.Environment(cr, SUPERUSER_ID, {}) + restore_env_managed_columns( + env, + "ir.mail_server", + ["smtp_host", "smtp_authentication"], + field_defaults={ + "smtp_authentication": "login", # fallback for required field + }, + ) + +The helper will use the fallback value if provided and the computed field value +is empty. If no fallback is provided but a required field has no value, a +``UserError`` is raised with instructions on how to provide a ``field_defaults`` +parameter. + +Migrating when dropping server_environment dependency +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When refactoring an existing addon that embeds a ``server.env.mixin`` binding, you +may want to extract the binding into a separate *glue* addon and drop the +``server_environment`` dependency from the original. This keeps the base addon +lightweight while preserving server-environment features for those who install +the glue addon. + +**Pattern:** + +- **Original addon (v1)**: depends on ``server_environment`` and binds the mixin + directly in model code. +- **Refactored addon (v2)**: removes ``server_environment`` from dependencies, + removes the mixin binding and the related ORM model inheritance. +- **New glue addon** (optional, same version): depends on both + ``server_environment`` and the original addon v2; re-adds the mixin binding in + a separate module file. + +**Migration checklist:** + +1. In the **original addon's v2** ``__manifest__.py``: + + - Remove ``"server_environment"`` from ``depends``. + - Remove the model file(s) that contained the mixin binding. + - Update ``depends`` to add the new glue addon *if* the base addon still needs + it (otherwise, make the glue addon optional for users who want env-binding). + +2. In the **original addon's v2 model code**: + + - Delete or simplify the model class that inherited from + ``server.env.mixin``. + - If the model was only there for the binding, remove it entirely. + - Restore the original field definitions (not as computed fields). + +3. **Create a migration script** (if needed) to restore columns *during the addon + upgrade*, before the ORM model extensions are unloaded. Use a ``@post_load`` + hook or a dedicated migration script:: + + # migrations/18.0.1.0.0/post-restore-columns.py + def migrate(cr, version): + # Call the restoration logic while the v1 model is still active + env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) + # If any field is required and may have no value in the environment, + # provide a fallback via field_defaults + restore_env_managed_columns( + env, + "storage.backend", + ["directory_path", "other_field"], + field_defaults={ + "directory_path": "/tmp", # fallback for required field + }, + ) + +4. **Create the glue addon** with the model re-inheritance:: + + # your_addon_env/__init__.py + from . import models + + # your_addon_env/models/__init__.py + from . import storage_backend + + # your_addon_env/models/storage_backend.py + class StorageBackend(models.Model): + _name = "storage.backend" + _inherit = ["storage.backend", "server.env.mixin"] + + @property + def _server_env_fields(self): + return {"directory_path": {}} + + # your_addon_env/__manifest__.py + { + "name": "Storage Backend – Server Environment", + "version": "18.0.1.0.0", + "depends": ["server_environment", "storage_backend"], + "installable": True, + } + +**Key points:** + +- Column restoration must happen *during the addon upgrade* (step 3), not as an + uninstall hook, because the original model binding is still active. +- The ``restore_env_managed_columns`` helper is idempotent and safe to call even + if columns already exist. +- Users who do not need server environment features simply do *not* install the + glue addon—the base addon continues to work with plain database columns. +- Users who do need server environment can install both the base addon (v2+) and + the glue addon (same version) to get the binding back. diff --git a/server_environment/static/description/index.html b/server_environment/static/description/index.html index 4c7df4d67..056472d27 100644 --- a/server_environment/static/description/index.html +++ b/server_environment/static/description/index.html @@ -372,7 +372,7 @@

server configuration environment files

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:5062bdb51631430aaf2892a874377786dfe3f0fc40b0e058f744cca1f7ffeaaa +!! source digest: sha256:ac1eeedab3b4cf31ddd2d67fab4968abd860e8bb7b53d28d9d7f4fd30cc6dcd6 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

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

This module provides a way to define an environment in the main Odoo @@ -398,13 +398,20 @@

server configuration environment files

  • Server environment integration
  • -
  • Usage
  • -
  • Known issues / Roadmap
  • -
  • Bug Tracker
  • -
  • Credits @@ -535,9 +542,163 @@

    Usage

    [...] +
    +

    Restoring columns on uninstall

    +

    When server.env.mixin is bound to an existing model, the ORM drops the +original stored columns for all env-managed fields. If the binding addon is +later uninstalled, those columns must be recreated so the database remains +usable.

    +

    Add an uninstall_hook to your addon and delegate to +restore_env_managed_columns:

    +
    +# your_addon/__init__.py
    +from .hooks import uninstall_hook
    +# your_addon/hooks.py
    +from odoo import SUPERUSER_ID, api
    +from odoo.addons.server_environment import uninstall
    +
    +def uninstall_hook(cr, registry):
    +    env = api.Environment(cr, SUPERUSER_ID, {})
    +    uninstall.restore_env_managed_columns(
    +        env,
    +        "storage.backend",
    +        ["directory_path", "other_field"],
    +    )
    +
    +# your_addon/__manifest__.py
    +{
    +    ...
    +    "uninstall_hook": "uninstall_hook",
    +}
    +
    +

    The helper creates any missing columns (idempotent: safe to call multiple +times) and repopulates them with each record’s current effective value — +whether that value came from an environment configuration file or from the +stored default field (x_<field>_env_default).

    +

    The hook must run before the ORM extensions are removed, which is guaranteed +by Odoo’s uninstall sequence (hooks execute before +Module.module_uninstall()).

    +
    +

    Handling required fields

    +

    If a restored column is required (has a NOT NULL constraint) but has no +effective value (missing from environment config and no default field set), the +restoration will fail with a UserError.

    +

    Solution: pass a field_defaults dictionary with fallback values:

    +
    +def uninstall_hook(cr, registry):
    +    env = api.Environment(cr, SUPERUSER_ID, {})
    +    restore_env_managed_columns(
    +        env,
    +        "ir.mail_server",
    +        ["smtp_host", "smtp_authentication"],
    +        field_defaults={
    +            "smtp_authentication": "login",  # fallback for required field
    +        },
    +    )
    +
    +

    The helper will use the fallback value if provided and the computed field value +is empty. If no fallback is provided but a required field has no value, a +UserError is raised with instructions on how to provide a field_defaults +parameter.

    +
    +
    +
    +

    Migrating when dropping server_environment dependency

    +

    When refactoring an existing addon that embeds a server.env.mixin binding, you +may want to extract the binding into a separate glue addon and drop the +server_environment dependency from the original. This keeps the base addon +lightweight while preserving server-environment features for those who install +the glue addon.

    +

    Pattern:

    + +

    Migration checklist:

    +
      +
    1. In the original addon’s v2 __manifest__.py:

      +
        +
      • Remove "server_environment" from depends.
      • +
      • Remove the model file(s) that contained the mixin binding.
      • +
      • Update depends to add the new glue addon if the base addon still needs +it (otherwise, make the glue addon optional for users who want env-binding).
      • +
      +
    2. +
    3. In the original addon’s v2 model code:

      +
        +
      • Delete or simplify the model class that inherited from +server.env.mixin.
      • +
      • If the model was only there for the binding, remove it entirely.
      • +
      • Restore the original field definitions (not as computed fields).
      • +
      +
    4. +
    5. Create a migration script (if needed) to restore columns during the addon +upgrade, before the ORM model extensions are unloaded. Use a @post_load +hook or a dedicated migration script:

      +
      +# migrations/18.0.1.0.0/post-restore-columns.py
      +def migrate(cr, version):
      +    # Call the restoration logic while the v1 model is still active
      +    env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
      +    # If any field is required and may have no value in the environment,
      +    # provide a fallback via field_defaults
      +    restore_env_managed_columns(
      +        env,
      +        "storage.backend",
      +        ["directory_path", "other_field"],
      +        field_defaults={
      +            "directory_path": "/tmp",  # fallback for required field
      +        },
      +    )
      +
      +
    6. +
    7. Create the glue addon with the model re-inheritance:

      +
      +# your_addon_env/__init__.py
      +from . import models
      +
      +# your_addon_env/models/__init__.py
      +from . import storage_backend
      +
      +# your_addon_env/models/storage_backend.py
      +class StorageBackend(models.Model):
      +    _name = "storage.backend"
      +    _inherit = ["storage.backend", "server.env.mixin"]
      +
      +    @property
      +    def _server_env_fields(self):
      +        return {"directory_path": {}}
      +
      +# your_addon_env/__manifest__.py
      +{
      +    "name": "Storage Backend – Server Environment",
      +    "version": "18.0.1.0.0",
      +    "depends": ["server_environment", "storage_backend"],
      +    "installable": True,
      +}
      +
      +
    8. +
    +

    Key points:

    + +
    -

    Known issues / Roadmap

    +

    Known issues / Roadmap

    -

    Bug Tracker

    +

    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 @@ -555,15 +716,15 @@

    Bug Tracker

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association diff --git a/server_environment/tests/test_server_environment.py b/server_environment/tests/test_server_environment.py index cfa6878c9..63bab62df 100644 --- a/server_environment/tests/test_server_environment.py +++ b/server_environment/tests/test_server_environment.py @@ -6,6 +6,8 @@ from odoo.tools.config import config as odoo_config +from odoo.addons.server_environment.uninstall import restore_env_managed_columns + from .. import server_env from . import common @@ -61,3 +63,14 @@ def test_value_retrival(self): self.assertEqual(val, "testing") val = parser.get("external_service.ftp", "host") self.assertEqual(val, "sftp.example.com") + + def test_restore_env_managed_columns_unknown_field(self): + """Helper gracefully skips a field that doesn't exist on the model.""" + # Must not raise even when the field name doesn't exist. + restore_env_managed_columns( + self.env, "res.partner", ["__nonexistent_field_xyz__"] + ) + + def test_restore_env_managed_columns_no_fields(self): + """Helper is a no-op when given an empty field list.""" + restore_env_managed_columns(self.env, "res.partner", []) diff --git a/server_environment/uninstall.py b/server_environment/uninstall.py new file mode 100644 index 000000000..0175e7511 --- /dev/null +++ b/server_environment/uninstall.py @@ -0,0 +1,96 @@ +# Copyright 2026 Camptocamp (https://www.camptocamp.com) +import logging + +from psycopg2 import sql as psycopg2_sql + +from odoo.tools import sql + +_logger = logging.getLogger(__name__) + + +def restore_env_managed_columns(env, model_name, field_names, field_defaults=None): + """Restore database columns for fields formerly managed via server.env.mixin. + + When an addon binds ``server.env.mixin`` to an existing model, the ORM + drops the original stored columns. Call this helper from an + ``uninstall_hook`` so those columns are recreated and repopulated + with their current effective values before the addon is removed. + + The hook must run *while* the module's ORM extensions are still active + (guaranteed by Odoo's uninstall sequence: hooks execute before + ``Module.module_uninstall()``), so the env-computed fields are still + readable and their values can be written back to freshly created columns. + + The operation is idempotent: calling it multiple times will not fail. + + **Defaults:** If a restored field value is NULL/empty, the helper will + use the fallback from ``field_defaults`` (if provided) or the field's + ORM-level default (if defined). + + Note: ``field.required`` is set to False by the mixin, so we cannot detect + which fields are required. Provide explicit ``field_defaults`` for fields + that must have values. + + :param str model_name: dotted model name, e.g. ``"ir.mail_server"`` + :param field_names: iterable of field names whose columns to restore + :param dict field_defaults: optional mapping of field name to fallback + value used when restoring a column that has no effective env-computed + value, e.g. ``{"smtp_authentication": ""}`` + """ + model = env[model_name] + cr = env.cr + field_defaults = field_defaults or {} + + for field_name in field_names: + field = model._fields.get(field_name) + if field is None: + _logger.warning( + "restore_env_managed_columns: field %r not found on %s, skipping", + field_name, + model_name, + ) + continue + column_type = field.column_type + if column_type is None: + _logger.warning( + "restore_env_managed_columns: " + "field %r on %s has no SQL column type, skipping", + field_name, + model_name, + ) + continue + table = model._table + if not sql.column_exists(cr, table, field_name): + sql.create_column(cr, table, field_name, column_type[1], field.string) + _logger.info( + "restore_env_managed_columns: created column %s.%s (%s)", + table, + field_name, + column_type[1], + ) + # Repopulate every existing record with the current computed value. + # The hook runs while the ORM extensions are still active, so the + # env-computed field is still readable via the normal accessor. + for record in model.search([]): + value = record[field_name] + # The ORM returns False for NULL on non-boolean fields; map + # that back to None so psycopg2 writes a proper SQL NULL. + if value is False and field.type != "boolean": + value = None + elif value == "": + value = None + + # Try to get a default value if we have None. + # Note: field.required is False after mixin transformation, + # so we apply defaults for all None values when available. + if value is None: + if field_name in field_defaults: + value = field_defaults[field_name] + elif field_name in model.default_get([field_name]): + value = model.default_get([field_name])[field_name] + + query = psycopg2_sql.SQL("UPDATE {} SET {} = %s WHERE id = %s").format( + psycopg2_sql.Identifier(table), + psycopg2_sql.Identifier(field_name), + ) + cr.execute(query, (value, record.id))