Skip to content

Commit 0ecea77

Browse files
committed
[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).
1 parent 5a2f8a2 commit 0ecea77

3 files changed

Lines changed: 191 additions & 1 deletion

File tree

server_environment/models/server_env_mixin.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from lxml import etree
88

99
from odoo import api, fields, models
10-
from odoo.tools import mute_logger
10+
from odoo.tools import SQL, mute_logger, sql
1111

1212
from odoo.addons.base_sparse_field.models.fields import Serialized
1313

@@ -428,3 +428,70 @@ def _setup_base(self):
428428
self._server_env_transform_field_to_read_from_env(field)
429429
self._server_env_add_is_editable_field(field)
430430
return
431+
432+
@api.model
433+
def restore_env_managed_columns(self, model_name, field_names):
434+
"""Restore database columns for fields formerly managed via server.env.mixin.
435+
436+
When an addon binds ``server.env.mixin`` to an existing model, the ORM
437+
drops the original stored columns. Call this helper from an
438+
``uninstall_hook`` so those columns are recreated and repopulated
439+
with their current effective values before the addon is removed.
440+
441+
The hook must run *while* the module's ORM extensions are still active
442+
(guaranteed by Odoo's uninstall sequence: hooks execute before
443+
``Module.module_uninstall()``), so the env-computed fields are still
444+
readable and their values can be written back to freshly created columns.
445+
446+
The operation is idempotent: calling it multiple times will not fail.
447+
448+
:param str model_name: dotted model name, e.g. ``"ir.mail_server"``
449+
:param field_names: iterable of field names whose columns to restore
450+
"""
451+
model = self.env[model_name]
452+
cr = self.env.cr
453+
for field_name in field_names:
454+
field = model._fields.get(field_name)
455+
if field is None:
456+
_logger.warning(
457+
"restore_env_managed_columns: field %r not found on %s, skipping",
458+
field_name,
459+
model_name,
460+
)
461+
continue
462+
column_type = field.column_type
463+
if column_type is None:
464+
_logger.warning(
465+
"restore_env_managed_columns: "
466+
"field %r on %s has no SQL column type, skipping",
467+
field_name,
468+
model_name,
469+
)
470+
continue
471+
table = model._table
472+
if not sql.column_exists(cr, table, field_name):
473+
sql.create_column(cr, table, field_name, column_type[1], field.string)
474+
_logger.info(
475+
"restore_env_managed_columns: created column %s.%s (%s)",
476+
table,
477+
field_name,
478+
column_type[1],
479+
)
480+
# Repopulate every existing record with the current computed value.
481+
# The hook runs while the ORM extensions are still active, so the
482+
# env-computed field is still readable via the normal accessor.
483+
for record in model.search([]):
484+
value = record[field_name]
485+
# The ORM returns False for NULL on non-boolean fields; map
486+
# that back to None so psycopg2 writes a proper SQL NULL.
487+
if value is False and field.type != "boolean":
488+
value = None
489+
cr.execute(
490+
SQL(
491+
"UPDATE %s SET %s = %s WHERE id = %s",
492+
SQL.identifier(table),
493+
SQL.identifier(field_name),
494+
value,
495+
record.id,
496+
)
497+
)

server_environment/readme/USAGE.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,115 @@ If you want to have a technical name to reference:
1919
_inherit = ["storage.backend", "server.env.techname.mixin"]
2020

2121
[...]
22+
23+
## Restoring columns on uninstall
24+
25+
When `server.env.mixin` is bound to an existing model, the ORM drops the
26+
original stored columns for all env-managed fields. If the binding addon is
27+
later uninstalled, those columns must be recreated so the database remains
28+
usable.
29+
30+
Add an `uninstall_hook` to your addon and delegate to
31+
`restore_env_managed_columns`:
32+
33+
# your_addon/__init__.py
34+
from . import models
35+
36+
def uninstall_hook(env):
37+
env["server.env.mixin"].restore_env_managed_columns(
38+
"storage.backend",
39+
["directory_path", "other_field"],
40+
)
41+
42+
# your_addon/__manifest__.py
43+
{
44+
...
45+
"uninstall_hook": "uninstall_hook",
46+
}
47+
48+
The helper creates any missing columns (idempotent: safe to call multiple
49+
times) and repopulates them with each record's current effective value —
50+
whether that value came from an environment configuration file or from the
51+
stored default field (`x_<field>_env_default`).
52+
53+
The hook must run *before* the ORM extensions are removed, which is guaranteed
54+
by Odoo's uninstall sequence (hooks execute before `Module.module_uninstall()`).
55+
56+
## Migrating when dropping server_environment dependency
57+
58+
When refactoring an existing addon that embeds a `server.env.mixin` binding, you
59+
may want to extract the binding into a separate *glue* addon and drop the
60+
`server_environment` dependency from the original. This keeps the base addon
61+
lightweight while preserving server-environment features for those who install
62+
the glue addon.
63+
64+
**Pattern:**
65+
66+
- **Original addon (v1)**: depends on `server_environment` and binds the mixin
67+
directly in model code.
68+
- **Refactored addon (v2)**: removes `server_environment` from dependencies,
69+
removes the mixin binding and the related ORM model inheritance.
70+
- **New glue addon** (optional, same version): depends on both `server_environment`
71+
and the original addon v2; re-adds the mixin binding in a separate module file.
72+
73+
**Migration checklist:**
74+
75+
1. In the **original addon's v2 `__manifest__.py`**:
76+
- Remove `"server_environment"` from `depends`.
77+
- Remove the model file(s) that contained the mixin binding.
78+
- Update `depends` to add the new glue addon *if* the base addon still needs it
79+
(otherwise, make the glue addon optional for users who want env-binding).
80+
81+
2. In the **original addon's v2 model code**:
82+
- Delete or simplify the model class that inherited from `server.env.mixin`.
83+
- If the model was only there for the binding, remove it entirely.
84+
- Restore the original field definitions (not as computed fields).
85+
86+
3. **Create a migration script** (if needed) to restore columns *during the addon
87+
upgrade*, before the ORM model extensions are unloaded. Use a `@post_load`
88+
hook or a dedicated migration script:
89+
90+
# migrations/18.0.1.0.0/post-restore-columns.py
91+
def migrate(cr, version):
92+
# Call the restoration logic while the v1 model is still active
93+
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
94+
env["server.env.mixin"].restore_env_managed_columns(
95+
"storage.backend",
96+
["directory_path", "other_field"],
97+
)
98+
99+
4. **Create the glue addon** with the model re-inheritance:
100+
101+
# your_addon_env/__init__.py
102+
from . import models
103+
104+
# your_addon_env/models/__init__.py
105+
from . import storage_backend
106+
107+
# your_addon_env/models/storage_backend.py
108+
class StorageBackend(models.Model):
109+
_name = "storage.backend"
110+
_inherit = ["storage.backend", "server.env.mixin"]
111+
112+
@property
113+
def _server_env_fields(self):
114+
return {"directory_path": {}}
115+
116+
# your_addon_env/__manifest__.py
117+
{
118+
"name": "Storage Backend – Server Environment",
119+
"version": "18.0.1.0.0",
120+
"depends": ["server_environment", "storage_backend"],
121+
"installable": True,
122+
}
123+
124+
**Key points:**
125+
126+
- Column restoration must happen *during the addon upgrade* (step 3), not as an
127+
uninstall hook, because the original model binding is still active.
128+
- The `restore_env_managed_columns` helper is idempotent and safe to call even
129+
if columns already exist.
130+
- Users who do not need server environment features simply do *not* install the
131+
glue addon—the base addon continues to work with plain database columns.
132+
- Users who do need server environment can install both the base addon (v2+) and
133+
the glue addon (same version) to get the binding back.

server_environment/tests/test_server_environment.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,14 @@ def test_server_environment_disabled_overwrite_options_section_by_env(self):
146146
with self.set_config_dir("testfiles"):
147147
server_env._load_config()
148148
self.assertEqual(odoo_config["odoo_test_option"], "fake odoo config")
149+
150+
def test_restore_env_managed_columns_unknown_field(self):
151+
"""Helper gracefully skips a field that doesn't exist on the model."""
152+
# Must not raise even when the field name doesn't exist.
153+
self.env["server.env.mixin"].restore_env_managed_columns(
154+
"res.partner", ["__nonexistent_field_xyz__"]
155+
)
156+
157+
def test_restore_env_managed_columns_no_fields(self):
158+
"""Helper is a no-op when given an empty field list."""
159+
self.env["server.env.mixin"].restore_env_managed_columns("res.partner", [])

0 commit comments

Comments
 (0)