この記事は現在英語版のみです。翻訳は近日公開予定です。
You upgrade an Odoo module from 17.0/18.0 to 19.0 and views fail to load with:
ParseError: while parsing /opt/odoo/custom/sale_extension/views/sale_order_views.xml:35
Attribute 'attrs' is no longer supported in Odoo 19.0. Use direct attributes
'invisible', 'readonly', 'required' or 'column_invisible' instead.
Or the silent variant — views render but conditional readonly/invisible/required logic does not apply, leaving forms broken in subtle ways. Odoo 18.0 removed attrs and states for views; 19.0 made the removal hard and the parser refuses the legacy syntax. This guide is the migration playbook.
Quick Fix
Convert every attrs="{'readonly': [('state', '=', 'done')]}" to the new direct attribute form:
<!-- OLD (Odoo 16.0 and earlier) -->
<field name="price" attrs="{'readonly': [('state', '=', 'done')]}"/>
<!-- NEW (Odoo 17.0+) -->
<field name="price" readonly="state == 'done'"/>
<!-- OLD -->
<field name="commission" attrs="{'invisible': [('user_id', '=', False)]}"/>
<!-- NEW -->
<field name="commission" invisible="not user_id"/>
The new syntax is a Python expression evaluated client-side against the record's field values, not the legacy domain syntax.
Why This Happens
attrs was the way to declare conditional UI behaviour from Odoo's earliest days through 16.0. It used domain-tuple syntax: attrs="{'invisible': [('state', '=', 'done')]}". Odoo 17.0 introduced direct attribute syntax — invisible="state == 'done'" — and deprecated attrs. Odoo 18.0 deprecated more aggressively. Odoo 19.0 removed attrs and states entirely.
The reasons for the removal:
- Performance. Direct attributes evaluate as Python expressions in one pass;
attrswas a JSON-encoded domain that needed parsing for every record render. - Readability.
readonly="state in ('done', 'cancel')"reads as Python;attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"reads as a footgun. - Type safety. The new syntax can be statically validated more easily.
stateswas buggy. It only worked for thestatefield and silently failed for everything else. Removed entirely.
Step-by-Step Diagnosis
1. Inventory attrs usage.
grep -rn "attrs=" /opt/odoo/custom/ | wc -l
This is the size of your migration. Below 50 lines, you can do it manually. Above 200, automate.
2. Inventory states usage.
grep -rn "states=" /opt/odoo/custom/ | wc -l
Most teams used states only on the state field; conversion is straightforward.
3. Check Odoo's own modules for examples. Every official module is migrated. git diff 16.0..17.0 -- addons/sale/views/ shows the patterns Odoo's own team used.
4. Identify complex domains. Some attrs use |, &, ! operators that need careful conversion.
<!-- OLD: NOT (state in done OR cancel) -->
<field attrs="{'readonly': [['!', ['state', 'in', ['done', 'cancel']]]]}"/>
<!-- NEW -->
<field readonly="state not in ('done', 'cancel')"/>
5. Test rendered behaviour. After conversion, open every form in dev and verify conditional fields appear/disappear/enable/disable as expected.
Permanent Fix
Manual conversion patterns:
<!-- attrs invisible -->
<field attrs="{'invisible': [('a', '=', 'x')]}"/>
<!-- becomes -->
<field invisible="a == 'x'"/>
<!-- attrs readonly -->
<field attrs="{'readonly': [('state', '=', 'done')]}"/>
<!-- becomes -->
<field readonly="state == 'done'"/>
<!-- attrs required -->
<field attrs="{'required': [('partner_id', '!=', False)]}"/>
<!-- becomes -->
<field required="bool(partner_id)"/>
<!-- compound: invisible AND readonly -->
<field attrs="{'invisible': [('a', '=', 'x')], 'readonly': [('b', '=', True)]}"/>
<!-- becomes -->
<field invisible="a == 'x'" readonly="b"/>
<!-- states -->
<button states="draft,sent" name="action_confirm"/>
<!-- becomes -->
<button invisible="state not in ('draft', 'sent')" name="action_confirm"/>
<!-- column_invisible (only in lists) -->
<field attrs="{'column_invisible': [('parent.show_a', '=', False)]}"/>
<!-- becomes -->
<field column_invisible="not parent.show_a"/>
Automated conversion script. For large codebases, write a one-off migration:
# convert_attrs.py — run with python3 convert_attrs.py path/to/views/
import re
import sys
from pathlib import Path
ATTRS_PATTERN = re.compile(
r"""attrs\s*=\s*"(\{[^"]+\})" """,
re.VERBOSE,
)
# Map of common attrs domains to Python expressions
# (left as exercise; full conversion needs a real parser)
for f in Path(sys.argv[1]).rglob('*.xml'):
content = f.read_text()
# ... apply conversions ...
f.write_text(new_content)
For production-grade conversion, the OCA migration_tools repo ships a tested converter — use it instead of rolling your own.
For complex domains with operators, manual review is required. The conversion '|' → or, '&' → and, '!' → not, but operator placement differs between domain syntax and Python.
Testing: After conversion, install and use the module as both admin and a non-admin user. Click through every state transition. Anything that used to be readonly/invisible should still be — if not, your conversion missed a case.
How to Prevent It
- Convert before you have to. Even on Odoo 17.0/18.0,
attrsis deprecated. Convert during the next minor refactor of any view file. By the time you upgrade to 19.0, the conversion is already done. - Pre-commit hook to ban new
attrs. A simple regex check that fails on newattrs=introductions stops the bleed:
#!/usr/bin/env bash
# .githooks/pre-commit
if grep -rn 'attrs="' --include='*.xml' . ; then
echo "ERROR: attrs is deprecated. Use direct invisible/readonly/required attributes."
exit 1
fi
- CI that boots the upgraded Odoo. A CI job that boots Odoo 19.0 against a fresh DB with your modules installed catches lingering
attrsfailures before merge. - OCA migration tools first. Use
migration_toolsfor the automated conversion. Manual conversion is fine for under 50 occurrences but error-prone above that. - Watch the Odoo release notes. Each major version retires deprecated features. Reading the release notes early gives you 6 to 12 months of buffer to migrate.
- Visual regression test the views. A Playwright test that screenshots key forms and compares them across Odoo versions catches subtle behaviour drift.
Related Errors
- Tree view shows readonly but allows edits — sibling view-state bug.
- Form view renders blank — what happens when an attrs conversion fails.
- Button onclick fires no action — buttons that used
states=may stop firing. - Upgrade aborted, database corrupted — what happens when 19.0 upgrade hits unconverted attrs.
Frequently Asked Questions
Why did Odoo remove attrs instead of just deprecating it?
Performance. With direct attributes, the renderer parses one Python expression per attribute per field. With attrs, the renderer parsed a JSON dict, then a list of domain tuples, then evaluated each tuple — orders of magnitude more work for views with many conditional fields. Removal was the only way to cap the performance ceiling.
Can I keep attrs if I never plan to upgrade to 19.0?
Technically yes on 17.0/18.0, but you are accumulating debt. Every new field with attrs is a future migration cost. Convert during normal development to amortize the cost over time.
Are there performance benefits even on 17.0/18.0?
Yes. Direct attributes evaluate faster than attrs even on 17.0. For dense forms (many fields with conditional state), the difference is user-visible.
What about Studio's generated attrs?
Older Studio versions generated attrs. Open the Studio overlay, save it once on Odoo 17.0+, and Studio rewrites the customization in the new syntax. If your customer's database has Studio customizations from Odoo 16.0 or earlier, this re-save step is part of the upgrade.
Need help with a tricky Odoo error? ECOSIRE's Odoo experts have shipped 215+ modules — get expert help.
執筆者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
関連記事
Odoo フォーム ビューにカスタム ボタンを追加する方法 (2026)
Odoo 19 フォーム ビューにカスタム アクション ボタンを追加します: Python アクション メソッド、ビューの継承、条件付き可視性、確認ダイアログ。製造テスト済み。
Studio を使用せずに Odoo にカスタムフィールドを追加する方法 (2026)
Odoo 19 のカスタム モジュールを介してカスタム フィールドを追加します: モデル継承、ビュー拡張、計算フィールド、ストア/非ストアの決定。コードファースト、バージョン管理。
外部レイアウトを使用して Odoo にカスタム レポートを追加する方法
web.external_layout: QWeb テンプレート、paperformat、アクション バインディングを使用して、Odoo 19 でブランド化された PDF レポートを構築します。印刷ロゴ + フッターのオーバーライド付き。