What Changed in Odoo 19
Odoo 19 replaced the old attrs dictionary syntax with direct Python expression attributes. Instead of writing attrs="{'invisible': [('state', '=', 'draft')]}", you now write invisible="state == 'draft'". This is a significant breaking change that affects every view in every custom module.
New Syntax
Invisible
Control field and element visibility:
<!-- Old Odoo 17 syntax -->
<field name="notes" attrs="{'invisible': [('state', '!=', 'confirmed')]}"/>
<!-- New Odoo 19 syntax -->
<field name="notes" invisible="state != 'confirmed'"/>Readonly
Control field editability:
<!-- Old syntax -->
<field name="amount" attrs="{'readonly': [('state', '=', 'done')]}"/>
<!-- New syntax -->
<field name="amount" readonly="state == 'done'"/>Required
Conditional required fields:
<!-- Old syntax -->
<field name="reason" attrs="{'required': [('state', '=', 'cancel')]}"/>
<!-- New syntax -->
<field name="reason" required="state == 'cancel'"/>Expression Syntax Rules
Supported Operators
| Operator | Example | Meaning |
|---|---|---|
| == | state == 'draft' | Equals |
| != | state != 'done' | Not equals |
| >, <, >=, <= | amount > 0 | Comparison |
| and | state == 'draft' and amount > 0 | Both conditions |
| or | state == 'draft' or state == 'cancel' | Either condition |
| not | not is_company | Negation |
| in | state in ('draft', 'sent') | Value in list |
| not in | state not in ('done', 'cancel') | Value not in list |
Boolean Fields
Boolean fields work directly as expressions:
<!-- Show only when is_company is True -->
<field name="vat" invisible="not is_company"/>
<!-- Show only when is_company is False -->
<field name="parent_id" invisible="is_company"/>Relational Fields
Access related field values in expressions:
<!-- Check Many2one field -->
<field name="warehouse_id" invisible="not company_id"/>
<!-- Boolean from related record -->
<field name="tax_id" readonly="company_id.tax_lock_date"/>Complex Expressions
<!-- Multiple conditions -->
<field name="discount"
invisible="state != 'draft' or not is_discount_allowed"/>
<!-- Nested conditions -->
<group invisible="state == 'draft' and not partner_id">
<field name="delivery_date"/>
<field name="shipping_method"/>
</group>Static vs Dynamic
Static Invisible
Use True or False for unconditional visibility:
<!-- Always hidden (useful for technical fields needed in the view) -->
<field name="currency_id" invisible="True"/>
<!-- Equivalent to column_invisible in tree views -->
<field name="internal_ref" column_invisible="True"/>Column Invisible in Tree Views
Hide entire columns (not just values) in list views:
<field name="discount" column_invisible="parent.hide_discount"/>Applying to Groups and Pages
Invisible and readonly work on containers, not just fields:
<!-- Hide entire notebook page -->
<page string="Advanced" invisible="not is_admin">
<field name="advanced_setting1"/>
<field name="advanced_setting2"/>
</page>
<!-- Hide entire group -->
<group string="Shipping" invisible="delivery_method == 'pickup'">
<field name="shipping_address"/>
<field name="tracking_number"/>
</group>Migration from Odoo 17
Conversion Rules
- Remove the
attrs=wrapper entirely - Convert domain tuples to Python expressions
- Replace
|(OR) withor - Replace
&(AND) withand - Replace
('field', '=', True)with justfield - Replace
('field', '=', False)withnot field
Before and After Examples
<!-- Odoo 17 -->
attrs="{'invisible': ['|', ('state', '=', 'draft'), ('amount', '<=', 0)]}"
<!-- Odoo 19 -->
invisible="state == 'draft' or amount <= 0"Common Mistakes
- Using domain syntax —
invisible="[('state','=','draft')]"is wrong. Use Python expression syntax. - Missing quotes on strings —
invisible="state == draft"fails. Usestate == 'draft'. - Using = instead of == — Single
=is assignment, not comparison. Always use==. - XML escaping — Use
&&orandfor AND in XML attributes. Preferand/orkeywords.