Uninstalling one Odoo module can remove its dependents and their data
An Odoo module replacement passed a rehearsal’s record-count and accounting-total checks, yet the uninstall also removed a dependent addon that owned important business data. The application still loaded. The acceptance checks had missed the field that mattered.
Before replacing a module, inspect its reverse dependencies and data ownership. Rehearse the corrected transition on a restored copy, then compare the affected values by record identity before approving the live change.
Uninstall computes a reverse-dependency set #
In Odoo 18’s module implementation
, downstream_dependencies() recursively finds modules that depend on the selected modules. button_uninstall() marks both the selection and those dependents as to remove.
The same source describes module_uninstall() as removing database structures created by the module, including tables and columns. Actual deletion depends on the affected records and metadata; do not assume every business record disappears, or that surviving rows retain every field.
Generalize the replacement to a full addon, a smaller replacement, and an extension that depends on the full addon. Installing the smaller addon does not automatically redirect the extension’s dependency. Uninstalling the full addon still includes that extension in the removal set.
The first rehearsal had the wrong acceptance criteria #
The historical transition checked surviving records, aggregate totals, and registry health. Those checks passed while the dependent addon was also uninstalled. The user caught the missing preservation requirement and required a restore.
The subsequent investigation found a second problem: changing the extension’s manifest alone would be insufficient. The smaller replacement did not yet implement a field the extension required. That field’s behavior needed to be ported before changing the dependency.
The retained record includes the corrective restore and a later clone’s uninstall/registry-reload output. It ends with the corrected rehearsal and final verification still open. It does not prove a successful final transition or a completed before/after digest comparison.
Rehearse the field handoff explicitly #
A safer transition procedure is:
- Quiesce writes and capture a consistent database and filestore backup. Verify checksums and prove that the pair restores.
- Record installed module states, reverse dependencies, and the fields and records that must survive. Keep the addon source revisions with the backup.
- Implement the required field behavior in the replacement, including compute and reversal behavior where applicable. Version-control the dependent addon too.
- On an isolated restore, install the replacement and upgrade the dependent addon so the database’s module metadata reflects the new dependency. Inspect the removal set again before uninstalling the old addon.
- Complete the uninstall, reload the registry, and compare the required values and behavior. Investigate field metadata and ownership during the handoff; identical field names alone do not establish safe transfer.
Disable scheduled jobs and outbound integrations on the clone. A rehearsal should not send mail, contact payment services, or write to production storage.
Compare identity and value, not only counts #
For a small affected dataset, compare canonical exports directly. For larger exports, a digest is convenient if the query scope, ordering, and serialization remain identical.
This synthetic Python check demonstrates why counts and non-null counts are insufficient. It models two records whose field values were exchanged; it is not an Odoo migration test:
import hashlib
import json
def digest(rows):
payload = json.dumps(sorted(rows), separators=(",", ":"), ensure_ascii=True)
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
before = [(1, "2026-01-10"), (2, None), (3, "2026-01-20")]
after = [(1, "2026-01-20"), (2, None), (3, "2026-01-10")]
assert len(before) == len(after)
assert sum(value is not None for _, value in before) == 2
assert sum(value is not None for _, value in after) == 2
assert digest(before) == digest(list(reversed(before)))
assert digest(before) != digest(after)
print("counts match; identity/value digest detects the change")
For a real comparison, export the stable record ID alongside each affected field, preserve nulls, and normalize dates and other types explicitly. A missing column must fail the check. Also test compute behavior and important workflows: a value digest cannot prove those work.
Before your next uninstall, list which dependent addons and field values must remain, and make their survival a rehearsal acceptance criterion.