Skip to content

Odoo 19 Invisible & Readonly Syntax: Complete Guide

DeployMonkey Team · March 24, 2026 10 min read

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

OperatorExampleMeaning
==state == 'draft'Equals
!=state != 'done'Not equals
>, <, >=, <=amount > 0Comparison
andstate == 'draft' and amount > 0Both conditions
orstate == 'draft' or state == 'cancel'Either condition
notnot is_companyNegation
instate in ('draft', 'sent')Value in list
not instate 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) with or
  • Replace & (AND) with and
  • Replace ('field', '=', True) with just field
  • Replace ('field', '=', False) with not 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 syntaxinvisible="[('state','=','draft')]" is wrong. Use Python expression syntax.
  • Missing quotes on stringsinvisible="state == draft" fails. Use state == 'draft'.
  • Using = instead of == — Single = is assignment, not comparison. Always use ==.
  • XML escaping — Use &amp;&amp; or and for AND in XML attributes. Prefer and/or keywords.