From 398d5b1aca26e48dac660e53120066fc97c9120d Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Wed, 25 Mar 2026 15:40:12 +0100 Subject: [PATCH 1/3] [IMP] server_environment: module uninstallation Add a helper to manage the restoring of the database columns when a module using `server_environment` is uninstalled or the dependency on `server_environment` is dropped. Document how to use the helper in an uninstall script or in an upgrade script (if a new version of the addon drops the dependency). --- server_environment/models/server_env_mixin.py | 108 +++++++++++++- server_environment/readme/USAGE.md | 139 ++++++++++++++++++ .../tests/test_server_environment.py | 11 ++ 3 files changed, 256 insertions(+), 2 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 65f0d029a..5f09d97e3 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -6,8 +6,8 @@ from lxml import etree -from odoo import api, fields, models -from odoo.tools import mute_logger +from odoo import _, api, fields, models +from odoo.tools import SQL, mute_logger, sql from odoo.addons.base_sparse_field.models.fields import Serialized @@ -428,3 +428,107 @@ 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 restore_env_managed_columns(self, 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. + + **Required fields:** If a field is required (has a NOT NULL constraint + in the database), the helper needs a value for every record. If the + computed env-value is empty (no config, no default), the helper will + use the fallback from ``field_defaults`` if provided. If no fallback + is available, a ``UserError`` is raised explaining which field needs + a value. + + :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 required column that has no effective + env-computed value, e.g. ``{"smtp_authentication": ""}`` + :raises UserError: if a required column has no value and no fallback + is provided in ``field_defaults`` + """ + from odoo.exceptions import UserError + + model = self.env[model_name] + cr = self.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": + if field.type in ("integer", "float", "monetary"): + value = 0 + else: + value = None + + # Handle required (NOT NULL) columns with no value. + if value is None and field.required: + if field_name in field_defaults: + value = field_defaults[field_name] + else: + raise UserError( + _( + "Field %(field_name)s on %(model)s is required " + "but has no value. Provide a fallback in " + "field_defaults parameter." + ) + % { + "field_name": field_name, + "model": model_name, + } + ) + + cr.execute( + SQL( + "UPDATE %s SET %s = %s WHERE id = %s", + SQL.identifier(table), + SQL.identifier(field_name), + value, + record.id, + ) + ) diff --git a/server_environment/readme/USAGE.md b/server_environment/readme/USAGE.md index ed811ae09..c895765be 100644 --- a/server_environment/readme/USAGE.md +++ b/server_environment/readme/USAGE.md @@ -19,3 +19,142 @@ 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 . import models + + def uninstall_hook(env): + env["server.env.mixin"].restore_env_managed_columns( + "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(env): + env["server.env.mixin"].restore_env_managed_columns( + "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 + env["server.env.mixin"].restore_env_managed_columns( + "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/tests/test_server_environment.py b/server_environment/tests/test_server_environment.py index 822d4a7e0..07bfcba66 100644 --- a/server_environment/tests/test_server_environment.py +++ b/server_environment/tests/test_server_environment.py @@ -146,3 +146,14 @@ def test_server_environment_disabled_overwrite_options_section_by_env(self): with self.set_config_dir("testfiles"): server_env._load_config() self.assertEqual(odoo_config["odoo_test_option"], "fake odoo config") + + 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. + self.env["server.env.mixin"].restore_env_managed_columns( + "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.""" + self.env["server.env.mixin"].restore_env_managed_columns("res.partner", []) From 25af25072f616bc0eb8a6c0bbe9eda45a8f74821 Mon Sep 17 00:00:00 2001 From: Tomasz Walter Date: Thu, 4 Jun 2026 14:14:10 +0200 Subject: [PATCH 2/3] fixup! [IMP] server_environment: module uninstallation --- server_environment/models/server_env_mixin.py | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/server_environment/models/server_env_mixin.py b/server_environment/models/server_env_mixin.py index 5f09d97e3..ed5d200ce 100644 --- a/server_environment/models/server_env_mixin.py +++ b/server_environment/models/server_env_mixin.py @@ -6,7 +6,7 @@ from lxml import etree -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.tools import SQL, mute_logger, sql from odoo.addons.base_sparse_field.models.fields import Serialized @@ -445,23 +445,20 @@ def restore_env_managed_columns(self, model_name, field_names, field_defaults=No The operation is idempotent: calling it multiple times will not fail. - **Required fields:** If a field is required (has a NOT NULL constraint - in the database), the helper needs a value for every record. If the - computed env-value is empty (no config, no default), the helper will - use the fallback from ``field_defaults`` if provided. If no fallback - is available, a ``UserError`` is raised explaining which field needs - a value. + **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 required column that has no effective - env-computed value, e.g. ``{"smtp_authentication": ""}`` - :raises UserError: if a required column has no value and no fallback - is provided in ``field_defaults`` + value used when restoring a column that has no effective env-computed + value, e.g. ``{"smtp_authentication": ""}`` """ - from odoo.exceptions import UserError - model = self.env[model_name] cr = self.env.cr field_defaults = field_defaults or {} @@ -501,27 +498,18 @@ def restore_env_managed_columns(self, model_name, field_names, field_defaults=No # 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": - if field.type in ("integer", "float", "monetary"): - value = 0 - else: - value = None - - # Handle required (NOT NULL) columns with no value. - if value is None and field.required: + 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] - else: - raise UserError( - _( - "Field %(field_name)s on %(model)s is required " - "but has no value. Provide a fallback in " - "field_defaults parameter." - ) - % { - "field_name": field_name, - "model": model_name, - } - ) + elif field_name in model.default_get([field_name]): + value = model.default_get([field_name])[field_name] cr.execute( SQL( From b5008d3031c593959d5d8bf21d76444ee33eed24 Mon Sep 17 00:00:00 2001 From: Tomasz Walter Date: Thu, 4 Jun 2026 14:15:29 +0200 Subject: [PATCH 3/3] Pre-commit auto fixes --- server_environment/README.rst | 230 ++++++++++++++--- .../static/description/index.html | 234 +++++++++++++++--- 2 files changed, 386 insertions(+), 78 deletions(-) diff --git a/server_environment/README.rst b/server_environment/README.rst index 8cd88cc74..479c96132 100644 --- a/server_environment/README.rst +++ b/server_environment/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ====================================== server configuration environment files ====================================== @@ -17,7 +13,7 @@ server configuration environment files .. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png :target: https://odoo-community.org/page/development-status :alt: Production/Stable -.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--env-lightgray.png?logo=github @@ -100,19 +96,19 @@ You can edit the settings you need in the ``server_environment_files`` addon. The ``server_environment_files_sample`` can be used as an example: -- values common to all / most environments can be stored in the - ``default/`` directory using the .ini file syntax; -- each environment you need to define is stored in its own directory and - can override or extend default values; -- you can override or extend values in the main configuration file of - your instance; -- In some platforms (like odoo.sh where production config file is copied - to staging) it can be useful to overwrite options written in the - ``[options]`` section. You must allow the override by adding - ``server_environment_allow_overwrite_options_section = True`` to the - former ``odoo.cfg`` config file or through the environment variable: - ``export SERVER_ENVIRONMENT_ALLOW_OVERWRITE_OPTIONS_SECTION=True`` (if - both are set config file takes precedence). +- values common to all / most environments can be stored in the + ``default/`` directory using the .ini file syntax; +- each environment you need to define is stored in its own directory + and can override or extend default values; +- you can override or extend values in the main configuration file of + your instance; +- In some platforms (like odoo.sh where production config file is + copied to staging) it can be useful to overwrite options written in + the ``[options]`` section. You must allow the override by adding + ``server_environment_allow_overwrite_options_section = True`` to the + former ``odoo.cfg`` config file or through the environment variable: + ``export SERVER_ENVIRONMENT_ALLOW_OVERWRITE_OPTIONS_SECTION=True`` + (if both are set config file takes precedence). Environment variable -------------------- @@ -221,16 +217,176 @@ 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 . import models + + def uninstall_hook(env): + env["server.env.mixin"].restore_env_managed_columns( + "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(env): + env["server.env.mixin"].restore_env_managed_columns( + "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 + env["server.env.mixin"].restore_env_managed_columns( + "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 ====================== -- it is not possible to set the environment from the command line. A - configuration file must be used. -- the module does not allow to set low level attributes such as database - server, etc. -- server.env.techname.mixin's tech_name field could leverage the new - option for computable / writable fields and get rid of some onchange / - read / write code. +- it is not possible to set the environment from the command line. A + configuration file must be used. +- the module does not allow to set low level attributes such as + database server, etc. +- server.env.techname.mixin's tech_name field could leverage the new + option for computable / writable fields and get rid of some onchange + / read / write code. Bug Tracker =========== @@ -253,18 +409,18 @@ Authors Contributors ------------ -- Florent Xicluna (Wingo) -- Nicolas Bessi -- Alexandre Fayolle -- Daniel Reis -- Holger Brunn -- Leonardo Pistone -- Adrien Peiffer -- Thierry Ducrest -- Guewen Baconnier -- Thomas Binfeld -- Stéphane Bidoul -- Simone Orsi +- Florent Xicluna (Wingo) +- Nicolas Bessi +- Alexandre Fayolle +- Daniel Reis +- Holger Brunn +- Leonardo Pistone +- Adrien Peiffer +- Thierry Ducrest +- Guewen Baconnier +- Thomas Binfeld +- Stéphane Bidoul +- Simone Orsi Maintainers ----------- diff --git a/server_environment/static/description/index.html b/server_environment/static/description/index.html index 27b282c3c..e61ba3573 100644 --- a/server_environment/static/description/index.html +++ b/server_environment/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +server configuration environment files -
+
+

server configuration environment files

- - -Odoo Community Association - -
-

server configuration environment files

-

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

+

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 configuration file and to read some configurations from files depending on the configured environment: you define the environment in the main @@ -398,19 +393,26 @@

server configuration environment files

  • Server environment integration
  • -
  • Usage
  • -
  • Known issues / Roadmap
  • -
  • Bug Tracker
  • -
  • Credits
  • -

    Installation

    +

    Installation

    By itself, this module does little. See for instance the mail_environment addon which depends on this one to allow configuring the incoming and outgoing mail servers depending on the @@ -422,7 +424,7 @@

    Installation

    SERVER_ENV_CONFIG and SERVER_ENV_CONFIG_SECRET.

    -

    Configuration

    +

    Configuration

    To configure this module, you need to edit the main configuration file of your instance, and add a directive called running_env. Commonly used values are ‘dev’, ‘test’, ‘production’:

    @@ -441,28 +443,28 @@

    Configuration

    If you don’t provide any value, test is used as a safe default.

    You have several possibilities to set configuration values:

    -

    server_environment_files

    +

    server_environment_files

    You can edit the settings you need in the server_environment_files addon. The server_environment_files_sample can be used as an example:

    • values common to all / most environments can be stored in the default/ directory using the .ini file syntax;
    • -
    • each environment you need to define is stored in its own directory and -can override or extend default values;
    • +
    • each environment you need to define is stored in its own directory +and can override or extend default values;
    • you can override or extend values in the main configuration file of your instance;
    • -
    • In some platforms (like odoo.sh where production config file is copied -to staging) it can be useful to overwrite options written in the -[options] section. You must allow the override by adding +
    • In some platforms (like odoo.sh where production config file is +copied to staging) it can be useful to overwrite options written in +the [options] section. You must allow the override by adding server_environment_allow_overwrite_options_section = True to the former odoo.cfg config file or through the environment variable: -export SERVER_ENVIRONMENT_ALLOW_OVERWRITE_OPTIONS_SECTION=True (if -both are set config file takes precedence).
    • +export SERVER_ENVIRONMENT_ALLOW_OVERWRITE_OPTIONS_SECTION=True +(if both are set config file takes precedence).
    -

    Environment variable

    +

    Environment variable

    You can define configuration in the environment variable SERVER_ENV_CONFIG and/or SERVER_ENV_CONFIG_SECRET. The 2 variables are handled the exact same way, this is only a convenience for @@ -512,7 +514,7 @@

    Environment variable

    reference records. See “USAGE”.
    -

    Default values

    +

    Default values

    When using the server.env.mixin mixin, for each env-computed field, a companion field <field>_env_default is created. This field is not environment-dependent. It’s a fallback value used when no key is set in @@ -521,7 +523,7 @@

    Default values

    Note: empty environment keys always take precedence over default fields

    -

    Server environment integration

    +

    Server environment integration

    Read the documentation of the class models/server_env_mixin.py and [models/server_env_tech_name_mixin.py] @@ -529,7 +531,7 @@

    Server environment integration

    -

    Usage

    +

    Usage

    You can include a mixin in your model and configure the env-computed fields by an override of _server_env_fields.

    @@ -551,21 +553,172 @@ 

    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 . import models
    +
    +def uninstall_hook(env):
    +    env["server.env.mixin"].restore_env_managed_columns(
    +        "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(env):
    +    env["server.env.mixin"].restore_env_managed_columns(
    +        "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. +
    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
      +    env["server.env.mixin"].restore_env_managed_columns(
      +        "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:

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

    +

    Known issues / Roadmap

    • it is not possible to set the environment from the command line. A configuration file must be used.
    • -
    • the module does not allow to set low level attributes such as database -server, etc.
    • +
    • the module does not allow to set low level attributes such as +database server, etc.
    • server.env.techname.mixin’s tech_name field could leverage the new -option for computable / writable fields and get rid of some onchange / -read / write code.
    • +option for computable / writable fields and get rid of some onchange +/ read / write code.
    -

    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 @@ -573,15 +726,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 @@ -611,6 +764,5 @@

    Maintainers

    -