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.
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.
<!-- 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
| Odoo version | attrs status | Action required |
|---|---|---|
| Odoo 15 (and earlier) | Supported — primary syntax | None. attrs is the standard approach. |
| Odoo 16 | Deprecated — logs a migration warning. Still functional. | Begin migrating: new direct attributes work in parallel. |
| Odoo 17 | Removed — raises XML view parse error. | Migration mandatory before upgrading to Odoo 17. |
| Odoo 18 & 19 | Removed — 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.
| Attribute | Applies to | Expression context | Notes |
|---|---|---|---|
| invisible | field, button, group, div, page, notebook | Current record field values | Hides the element. Does not send value to server when hidden. |
| readonly | field, group | Current record field values | Renders field as non-editable. Value is still submitted. |
| required | field | Current record field values | Adds client-side validation. Server-side constraint still recommended. |
| column_invisible | field inside tree/list view | parent.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
| 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 view | column_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
<!-- 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
<!-- 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
<!-- 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
<!-- 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.
<!-- 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
| Odoo version | attrs | states | invisible / readonly / required | column_invisible | tree vs. list |
|---|---|---|---|---|---|
| Odoo 15 | ✓ Supported | ✓ Supported | Not available | Not available | tree only |
| Odoo 16 | ⚠ Deprecated (warning in logs) | ⚠ Deprecated | ✓ Introduced | ✓ Introduced | list (tree alias works) |
| Odoo 17 | ✗ Removed (XML parse error) | ✗ Removed | ✓ Required | ✓ Required for columns | list (tree alias retained) |
| Odoo 18 & 19 | ✗ Removed | ✗ Removed | ✓ Standard | ✓ Standard | list (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.
Frequently Asked Questions
Will my Odoo 15 module with attrs still work on Odoo 16?
What is the difference between invisible and column_invisible?
Can I use Python functions like len() or isinstance() inside invisible expressions?
How do I make a group or page tab invisible in a form view?
I upgraded to Odoo 17 and all my custom views show a blank form. What happened?
Does the domain attribute on a relational field also need to be migrated?

iWesabe Editorial Team
Practitioner insights on Odoo ERP, ZATCA compliance, and Saudi enterprise digital operations — written by iWesabe's consulting, finance, and engineering teams.
Related Articles
How to Create a Custom Snippet in Odoo Website
Step-by-step guide to building reusable drag-and-drop building blocks for the Odoo Website editor — from XML template to live snippet panel. Verified for Odoo 18 & 19.
Context and Domain in Odoo — Developer Reference (Odoo 16–19)
A practical guide to Odoo domains (record filtering) and context (key–value state passing) — how they work, where they appear, and the syntax rules for Odoo 16 through 19. Verified for Odoo 18 & 19.
How to Add Colors to List View Rows and Cells in Odoo
Use decoration-* attributes to colour-code rows and individual cells in Odoo list views based on field values — with expression syntax, all decoration types, and a migration note for the tree → list rename. Verified for Odoo 18 & 19.
Explore Related Solutions
CRM & Customer Relationship Management
Turn every lead into a closed deal. Odoo CRM gives your sales team a structured Kanban pipeline, automated follow-up sequences, and a 360° customer view — all connected natively to Accounting, Inventory, and Email Marketing.
ExploreFinancial Management
Automate your Saudi Arabia accounting — ZATCA Phase 2 e-invoicing, VAT returns, Zakat provisioning, multi-currency bank reconciliation, and real-time financial reporting — all inside one connected Odoo platform. Fully localised for KSA and the GCC by iWesabe.
ExploreManufacturing Management ERP
Run lean, traceable production with Odoo Manufacturing — work orders, Bills of Materials, quality control, and real-time OEE dashboards built for KSA factory floors.
ExploreSupply Chain Management
From purchase order to final delivery — Odoo Supply Chain connects procurement, multi-warehouse inventory, demand forecasting, and vendor management into one real-time platform. Purpose-configured for Saudi Arabia and the GCC by iWesabe.
Explore