A partner, a product, a contact — it existed yesterday, nobody deleted it, and today a search for it comes back empty. It’s not gone. It’s archived, and Odoo’s default search behavior excludes archived records from every query that doesn’t explicitly say otherwise, silently, with no indication in the empty result that anything was filtered out at all.

The short version: a boolean field literally named active gets special treatment from the ORM — every default search()/search_read() implicitly excludes active=False records, with no error and no visible sign that filtering happened. Getting archived records back requires either active_test: False in the context, or an explicit domain naming active directly.

Why “archive” exists instead of just deleting

Odoo’s UI calls it “archive,” and that’s exactly what it is: a soft-delete convention built entirely on one specially-recognized boolean field named active. Archiving a record doesn’t touch a single other column — it’s still there, with every other field intact, related records still pointing at it — it’s just marked active=False, and Odoo’s default search behavior treats that as “don’t show me this unless I ask.”

This matters for anything that references the record: a sale.order.line’s product_id pointing at an archived product still resolves fine (the relation itself doesn’t care about active), but a search for that product in a new order’s product picker won’t find it, since that picker’s search is a default one.

The part that actually surprises people: it’s completely silent

Neither search() nor search_read() raises a warning, logs anything, or returns a count reflecting how many records were filtered out — an archived record simply isn’t in the result, indistinguishable from a record that was never created at all. There’s no flag in the response saying “3 matching records were excluded because they’re archived.” The only way to know archiving is even a factor is to already know the model has an active field and that some records on it are set to False.

Getting archived records back

Two ways to include archived records, doing the same thing at different layers:

# Context flag — affects every search performed with this environment,
# including ones triggered indirectly by other code you didn't write.
self.env['product.template'].with_context(active_test=False).search([])

# Explicit domain — scoped to exactly this one call, regardless of context.
self.env['product.template'].search([('active', '=', False)])

# Both active and archived together, in one result:
self.env['product.template'].with_context(active_test=False).search(
    ['|', ('active', '=', True), ('active', '=', False)]
)

active_test: False in the context disables the automatic filter entirely for that call (and anything downstream that reuses the same environment) — including only archived records, or including both, then depends entirely on what domain you actually pass alongside it. Explicitly naming active in a domain (either value) also disables the automatic filter for that call specifically, without needing the context flag at all, since an explicit condition on active overrides the implicit one rather than stacking with it.

Watch out: with_context(active_test=False) on a recordset propagates to further ORM calls made through it — a `one2many`/`many2many` field read through a context-modified parent can start silently including archived children too, in code that never explicitly asked for that. Scope it to exactly the call that needs it rather than leaving it set on a broadly-reused environment.

Building this in the Domain Filter

The exact ['|', ('active', '=', True), ('active', '=', False)] shape above is a real domain, buildable and explainable the same as any other — paste it into the Domain Filter Builder & Explainer’s Explainer mode to see the OR-of-both-values structure spelled out in plain English, or use Builder mode to construct the “show archived and active together” filter for a custom search view without hand-writing the prefix notation.

Quick reference

GoalHow
Default behavior — active records onlyNo change needed, this is what happens automatically
Only archived recordssearch([('active', '=', False)])
Both active and archived`with_context(active_test=False).search(['
Archived records through a relation (e.g. a one2many)Add context={'active_test': False} on the field definition itself, or query explicitly with the flag

Frequently asked questions

Does every model have an active field?

No — only models that define one. A model with no active field at all has no archiving behavior; this entire mechanism is opt-in per model, triggered purely by the field's exact name.

Is archiving the same as the record rules from the access-control article?

No — record rules are a per-group access-control mechanism; active filtering is a universal default applied to every search regardless of who's asking, purely about active vs. archived status, not about who's allowed to see what.

Can I make a model never filter by active by default?

The automatic filtering is tied to the field being named exactly active — a model author choosing that name is opting into the convention; there's no separate flag to disable it while keeping the field named active, since the name itself is what the ORM checks for.

Does archiving cascade to related records?

No — archiving a record only sets that record's own active field. Any child records (a one2many's rows, for instance) keep their own active status entirely independently unless a module explicitly adds logic to cascade it.

Why does a one2many field on a form sometimes show archived children and sometimes not?

Whether archived children appear depends on whether that specific field's definition sets context={'active_test': False} — some Odoo core fields do this deliberately (so a form can show an archived line item that's still historically relevant), others don't, and it's a per-field choice rather than a model-wide setting.

Further reading

And on this site: the Domain Filter Builder & Explainer for building or explaining the exact OR-domain shown above, and Why Can’t This User See a Record They Should? for the separate, per-group mechanism this one is often confused with.