跳至主要内容

Odoo 属性和状态 已于 19 年弃用 — 迁移指南

Odoo 19.0 中删除了 attrs 和 states?使用此分步迁移指南和 lint 模式转换为直接属性语法,以实现安全生产。

E
ECOSIRE Research and Development Team
|2026年5月4日6 分钟阅读1.1k 字数|

本文目前仅提供英文版本。翻译即将推出。

You upgrade an Odoo module from 17.0/18.0 to 19.0 and views fail to load with:

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:

  1. Performance. Direct attributes evaluate as Python expressions in one pass; attrs was a JSON-encoded domain that needed parsing for every record render.
  2. Readability. readonly="state in ('done', 'cancel')" reads as Python; attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}" reads as a footgun.
  3. Type safety. The new syntax can be statically validated more easily.
  4. states was buggy. It only worked for the state field 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, attrs is 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 new attrs= 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 attrs failures before merge.
  • OCA migration tools first. Use migration_tools for 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.

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.

分享:
E

作者

ECOSIRE Research and Development Team

技术写作

ECOSIRE 技术写作团队的内容涵盖 Odoo ERP、Shopify 电子商务、AI 智能体、Power BI 分析、GoHighLevel 自动化以及企业软件最佳实践。我们的指南帮助企业做出明智的技术决策。

ECOSIRE

使用 Odoo ERP 转变您的业务

专家 Odoo 实施、定制和支持可简化您的运营。