Odoo Development

Odoo attrs Deprecated: Migrating to invisible, readonly, and required in Odoo 16–19

The attrs dictionary was deprecated in Odoo 16 and removed in Odoo 17. This guide maps every attrs pattern to its direct-attribute replacement — with before/after code for the most common use cases. Verified for Odoo 18 & 19.

iWesabe Editorial TeamMay 2, 20227 min read

In Odoo 15 and earlier, controlling whether a form field was visible, read-only, or required based on other field values required the `attrs` dictionary — a JSON-like attribute that bundled multiple conditions into one place. Odoo 16 introduced a cleaner approach: direct `invisible`, `readonly`, `required`, and `column_invisible` attributes that accept Python-like expressions. This guide covers what changed, why, and how to migrate every common `attrs` pattern.

What Was the attrs Attribute?

`attrs` was an XML attribute accepted on field, button, and group elements in Odoo form, list, and search views. It took a Python dictionary (written as a JSON string) mapping three behaviour keys — `invisible`, `readonly`, `required` — to Odoo domain expressions that evaluated against the current record's field values.

xml
<!-- Odoo 15 and earlier — attrs syntax (DO NOT USE in Odoo 16+) -->
<field name="amount_total"
       attrs="{
           'invisible': [('state', '=', 'draft')],
           'readonly':  [('state', 'in', ['done', 'cancel'])],
           'required':  [('invoice_type', '=', 'out_invoice')]
       }"/>

Deprecation Timeline

attrs deprecation and removal across Odoo versions
Odoo versionattrs statusAction required
Odoo 15 (and earlier)Supported — primary syntaxNone. attrs is the standard approach.
Odoo 16Deprecated — logs a migration warning. Still functional.Begin migrating: new direct attributes work in parallel.
Odoo 17Removed — raises XML view parse error.Migration mandatory before upgrading to Odoo 17.
Odoo 18 & 19Removed — same error as Odoo 17.Use direct invisible / readonly / required / column_invisible only.

The New Syntax — Direct Attributes

From Odoo 16 onward, each behaviour is a separate XML attribute that accepts a Python expression string. The expression is evaluated in the context of the current record — all field values on the record are available as Python variables.

Direct attribute reference for Odoo 16–19
AttributeApplies toExpression contextNotes
invisiblefield, button, group, div, page, notebookCurrent record field valuesHides the element. Does not send value to server when hidden.
readonlyfield, groupCurrent record field valuesRenders field as non-editable. Value is still submitted.
requiredfieldCurrent record field valuesAdds client-side validation. Server-side constraint still recommended.
column_invisiblefield inside tree/list viewparent.field (use parent. prefix for header-level fields)Hides the entire column. Use instead of invisible for list view columns.

Migration Reference — attrs Patterns to New Attributes

Common attrs patterns and their Odoo 16–19 equivalents
attrs pattern (Odoo 15)Direct attribute (Odoo 16–19)Notes
attrs="{'invisible': [('state', '=', 'done')]}"invisible="state == 'done'"Simple equality check
attrs="{'invisible': [('state', 'in', ['done', 'cancel'])]}"invisible="state in ('done', 'cancel')"in operator — use tuple, not list
attrs="{'invisible': [('partner_id', '=', False)]}"invisible="not partner_id"Falsy check on a Many2one field
attrs="{'invisible': [('partner_id', '!=', False)]}"invisible="partner_id"Truthy check — field name alone is sufficient
attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"readonly="state in ('done', 'cancel')"Readonly when in a set of values
attrs="{'required': [('invoice_type', '=', 'out_invoice')]}"required="invoice_type == 'out_invoice'"Conditional required field
attrs="{'invisible': [('type', '!=', 'product'), ('state', '!=', 'draft')]}" (AND logic)invisible="type != 'product' and state != 'draft'"Domain AND → Python and
attrs="{'invisible': ['|', ('type', '=', 'service'), ('state', '=', 'done')]}" (OR logic)invisible="type == 'service' or state == 'done'"Domain OR (the '|' prefix) → Python or
Column hidden via invisible in tree viewcolumn_invisible="parent.state == 'draft'"Use column_invisible (not invisible) for list view columns; reference parent record via parent.

Before / After Code Examples

Example 1 — Form View Field with Multiple Behaviours

xml
<!-- BEFORE — Odoo 15 (attrs, do not use in Odoo 16+) -->
<field name="amount_total"
       attrs="{
           'invisible': [('state', '=', 'draft')],
           'readonly':  [('state', 'in', ['done', 'cancel'])],
           'required':  [('invoice_type', '=', 'out_invoice')]
       }"/>

<!-- AFTER — Odoo 16 / 17 / 18 / 19 -->
<field name="amount_total"
       invisible="state == 'draft'"
       readonly="state in ('done', 'cancel')"
       required="invoice_type == 'out_invoice'"/>

Example 2 — List View Column Visibility

xml
<!-- BEFORE — Odoo 15 (attrs in tree view) -->
<tree>
    <field name="name"/>
    <field name="amount" attrs="{'invisible': [('state', '=', 'draft')]}"/>
    <field name="state"/>
</tree>

<!-- AFTER — Odoo 16+ (column_invisible for columns) -->
<list>  <!-- tree was renamed to list in Odoo 16 -->
    <field name="name"/>
    <field name="amount" column_invisible="parent.state == 'draft'"/>
    <field name="state"/>
</list>

Example 3 — OR Logic

xml
<!-- BEFORE — Odoo 15 (OR domain with '|' prefix) -->
<field name="discount"
       attrs="{'invisible': ['|', ('type', '=', 'service'), ('state', '=', 'done')]}"/>

<!-- AFTER — Odoo 16+ (Python or) -->
<field name="discount"
       invisible="type == 'service' or state == 'done'"/>

Example 4 — Button Visibility

xml
<!-- BEFORE — Odoo 15 -->
<button name="action_confirm" string="Confirm"
        attrs="{'invisible': [('state', '!=', 'draft')]}"/>

<!-- AFTER — Odoo 16+ -->
<button name="action_confirm" string="Confirm"
        invisible="state != 'draft'"/>

The states Attribute — Also Deprecated

Beyond `attrs`, Odoo 15 also supported a `states` shorthand attribute that set visibility based on the record's `state` field. It was deprecated in Odoo 16 alongside `attrs`. Replace it with `invisible` using the same expression pattern.

xml
<!-- BEFORE — Odoo 15 (states shorthand) -->
<button name="action_confirm" string="Confirm" states="draft"/>

<!-- AFTER — Odoo 16+ -->
<button name="action_confirm" string="Confirm"
        invisible="state != 'draft'"/>

Finding attrs in Your Codebase

Version Notes

View expression syntax across Odoo versions
Odoo versionattrsstatesinvisible / readonly / requiredcolumn_invisibletree vs. list
Odoo 15✓ Supported✓ SupportedNot availableNot availabletree only
Odoo 16⚠ Deprecated (warning in logs)⚠ Deprecated✓ Introduced✓ Introducedlist (tree alias works)
Odoo 17✗ Removed (XML parse error)✗ Removed✓ Required✓ Required for columnslist (tree alias retained)
Odoo 18 & 19✗ Removed✗ Removed✓ Standard✓ Standardlist (tree alias retained)

Upgrading Your Odoo Custom Modules to Odoo 17, 18, or 19?

Our Odoo-certified team handles module migration, attrs-to-expression refactors, and full upgrade testing for Saudi businesses — including ZATCA and GOSI compliance verification on the new version.

WhatsApp

Frequently Asked Questions

Will my Odoo 15 module with attrs still work on Odoo 16?
Yes, but with deprecation warnings in the server logs. Odoo 16 still parses and evaluates `attrs` — it logs a warning for each view element using the deprecated syntax. The module will function, but you should migrate `attrs` to direct attributes before upgrading to Odoo 17, where the attribute is removed entirely and causes a parse error.
What is the difference between invisible and column_invisible?
`invisible` on a field inside a list/tree view hides the cell value for a specific row but keeps the column visible (the column header still shows). `column_invisible` hides the entire column, including the header, for all rows. Use `column_invisible` when the column should not appear at all based on the parent record's state. Use `invisible` when only certain rows should hide the value.
Can I use Python functions like len() or isinstance() inside invisible expressions?
No. The expression sandbox is intentionally limited — it supports field values, comparison operators (`==`, `!=`, `>`, `<`, `>=`, `<=`), logical operators (`and`, `or`, `not`), the `in` and `not in` operators, and Python literals. Full Python built-ins like `len()`, `isinstance()`, or list comprehensions are not available. If you need complex logic, compute the result in a `@api.depends` computed field and reference that field in the expression.
How do I make a group or page tab invisible in a form view?
Apply `invisible` directly on the ``, ``, or `` element — the same expression syntax works for containers, not just fields. Example: ``. All fields inside a hidden group or page are automatically hidden with it and their values are not submitted.
I upgraded to Odoo 17 and all my custom views show a blank form. What happened?
Odoo 17 raises an XML parse error when it encounters `attrs` — this typically causes the view to fail silently, rendering as empty. Check the Odoo server log for lines containing `Invalid field` or `Unknown attribute attrs`. Run `grep -rn 'attrs=' --include='*.xml' .` from your addons directory to locate all occurrences, then migrate them to direct attributes using the conversion table in this guide.
Does the domain attribute on a relational field also need to be migrated?
No. The `domain` attribute on Many2one, Many2many, and One2many fields is separate from the deprecated `attrs` dictionary. It filters the dropdown options shown when the user selects a related record, and it continues to use the standard Odoo domain list syntax in Odoo 16–19. Only the `attrs` dictionary (and the `states` shorthand) needed migration — `domain` for filtering is unchanged.
iWesabe Editorial Team

iWesabe Editorial Team

Practitioner insights on Odoo ERP, ZATCA compliance, and Saudi enterprise digital operations — written by iWesabe's consulting, finance, and engineering teams.

About iWesabe

Related Articles