What Changed
In Odoo 17, the attrs attribute was completely removed from XML views. This was the single biggest breaking change in years because attrs was used everywhere — in virtually every custom module and every view customization.
Before (Odoo 16 and earlier)
<field name="discount"
attrs="{'invisible': [('state', '!=', 'draft')],
'readonly': [('state', '==', 'done')],
'required': [('type', '=', 'service')]}"/>After (Odoo 17+)
<field name="discount"
invisible="state != 'draft'"
readonly="state == 'done'"
required="type == 'service'"/>Key Differences
| Aspect | Old (attrs) | New (direct) |
|---|---|---|
| Syntax | Python domain: [('field', '=', 'value')] | Python expression: field == 'value' |
| AND | [('a', '=', 1), ('b', '=', 2)] | a == 1 and b == 2 |
| OR | ['|', ('a', '=', 1), ('b', '=', 2)] | a == 1 or b == 2 |
| NOT | [('field', '!=', True)] | not field |
| IN | [('state', 'in', ['a', 'b'])] | state in ('a', 'b') |
| Boolean | [('active', '=', True)] | active |
| Negation | [('active', '=', False)] | not active |
Migration Examples
Simple invisible
<!-- Old -->
<field name="notes" attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<!-- New -->
<field name="notes" invisible="state != 'draft'"/>Multiple conditions (AND)
<!-- Old -->
<field name="discount" attrs="{'invisible': [('state', '!=', 'draft'), ('is_manager', '=', False)]}"/>
<!-- New -->
<field name="discount" invisible="state != 'draft' and not is_manager"/>OR conditions
<!-- Old -->
<field name="notes" attrs="{'invisible': ['|', ('state', '=', 'done'), ('state', '=', 'cancel')]}"/>
<!-- New -->
<field name="notes" invisible="state == 'done' or state == 'cancel'"/>
<!-- Or more concisely -->
<field name="notes" invisible="state in ('done', 'cancel')"/>Combined invisible + readonly
<!-- Old -->
<field name="partner_id"
attrs="{'invisible': [('type', '=', 'internal')],
'readonly': [('state', '!=', 'draft')]}"/>
<!-- New -->
<field name="partner_id"
invisible="type == 'internal'"
readonly="state != 'draft'"/>On groups/pages/buttons
<!-- Old -->
<group attrs="{'invisible': [('state', '=', 'draft')]}">
...
</group>
<!-- New -->
<group invisible="state == 'draft'">
...
</group>
<!-- Buttons -->
<button name="action_confirm" string="Confirm"
invisible="state != 'draft'"/>Automated Migration
Claude Code can migrate entire modules automatically:
# Prompt:
"Migrate all attrs= attributes in my module's XML views
from Odoo 16 domain syntax to Odoo 17 direct attribute syntax.
Convert invisible, readonly, and required attrs."Claude Code reads all XML files, identifies every attrs= usage, converts the domain syntax to expression syntax, and replaces them — across all files in one pass.
Common Mistakes
- Forgetting to convert OR operators —
'|'prefix in domains becomesorin expressions - Wrong boolean syntax —
[('field', '=', True)]becomesfield(notfield == True) - String quoting — Use single quotes inside double-quoted attribute:
invisible="state == 'draft'" - Inherited views — If you inherit a core view that had attrs, your xpath might also need updating
Getting Started
If you are still on Odoo 16, plan your migration now. The attrs change affects every custom module. Use Claude Code for automated migration and test on a DeployMonkey staging instance before applying to production.