هذه المقالة متاحة حاليًا باللغة الإنجليزية فقط. الترجمة قريبا.
Odoo XML Views: Form, List, Kanban, and Search Customization
Every Odoo developer spends a measurable percentage of their time editing XML views. The catalog of view types isn't huge — form, list (tree), kanban, search, calendar, gantt, pivot, graph — but each has its own conventions, attributes, and inheritance patterns. Misuse them and your customizations either don't render at all or render in the wrong place because the xpath targeted the wrong node.
This article walks through the four most-used view types with concrete patterns from production modules. Covers Odoo 17/18/19; XML views haven't changed dramatically across versions but a few attributes have been deprecated in 19.
Key Takeaways
- Form, list, kanban, search are the four views you'll edit 90% of the time
- Inheritance via
<xpath>or attribute-based shortcuts (position="after"etc.)- View priority resolves conflicts when multiple inherits target the same view
attrs(deprecated in 19) becomes inlineinvisible="...",readonly="..."expressions- Decorations color rows/cards based on conditions
- Search views power filters, group-by, and quick search
- Always test view changes against the rendering modes (form on web, mobile, list-grouping)
View architecture
Every Odoo view is an ir.ui.view record with:
model— which model the view is forarch— the XML defining the layoutinherit_id— the parent view this inherits from (null for root views)priority— order of inheritance application (lower applies first)mode— primary or extension
For a model like res.partner, you typically have:
res.partner.form— the main form viewres.partner.list(orres.partner.tree) — the list viewres.partner.kanban— the kanban viewres.partner.search— the search view
When you customize, you almost always inherit and extend rather than replace.
Form view
The form view shows one record's fields:
<record id="view_my_model_form" model="ir.ui.view">
<field name="name">my.model.form</field>
<field name="model">my.model</field>
<field name="arch" type="xml">
<form>
<header>
<button name="action_approve" type="object" string="Approve"
class="oe_highlight" invisible="state != 'draft'"/>
<field name="state" widget="statusbar" statusbar_visible="draft,approved,done"/>
</header>
<sheet>
<div class="oe_title">
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="partner_id"/>
<field name="amount"/>
</group>
<group>
<field name="date"/>
<field name="user_id"/>
</group>
</group>
<notebook>
<page string="Details">
<field name="line_ids">
<list editable="bottom">
<field name="product_id"/>
<field name="qty"/>
<field name="price"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
Key conventions:
<header>for status bar and primary action buttons<sheet>wraps the main editable area (white card on neutral background)<group>organizes fields in two-column layout (group within group = side-by-side)<notebook>+<page>for tabbed sections<chatter/>adds the message thread, activity, and follower section (Odoo 19 syntax)
List (tree) view
The list view shows multiple records as rows. In Odoo 19, the tag changed from <tree> to <list> (both still work for backward compat):
<record id="view_my_model_list" model="ir.ui.view">
<field name="name">my.model.list</field>
<field name="model">my.model</field>
<field name="arch" type="xml">
<list decoration-info="state == 'draft'"
decoration-success="state == 'done'"
decoration-danger="state == 'cancel'">
<field name="name"/>
<field name="partner_id"/>
<field name="amount" sum="Total"/>
<field name="date"/>
<field name="state" widget="badge"/>
</list>
</field>
</record>
Key attributes:
decoration-X="condition"— color rows. X = info (blue), success (green), warning (yellow), danger (red), muted (gray), bf (bold), it (italic).sum="Label"on a numeric field — shows column sum at bottomeditable="top"oreditable="bottom"— inline editingmulti_edit="1"— bulk edit selected recordsdefault_order="field desc"— initial sort
Kanban view
Kanban shows records as cards organized in columns:
<record id="view_my_model_kanban" model="ir.ui.view">
<field name="name">my.model.kanban</field>
<field name="model">my.model</field>
<field name="arch" type="xml">
<kanban default_group_by="state">
<field name="state"/>
<field name="partner_id"/>
<field name="amount"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<div class="o_kanban_record_top">
<strong><field name="name"/></strong>
</div>
<div class="o_kanban_record_body">
<field name="partner_id"/>
<field name="amount" widget="monetary"/>
</div>
<div class="o_kanban_record_bottom">
<span t-if="record.state.raw_value == 'done'">
<i class="fa fa-check text-success"/>
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
The QWeb template inside <templates> renders each card. record.field_name.raw_value gives the underlying value; record.field_name.value gives the display value.
Search view
Search views drive filters and group-by:
<record id="view_my_model_search" model="ir.ui.view">
<field name="name">my.model.search</field>
<field name="model">my.model</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<field name="partner_id"/>
<separator/>
<filter name="my_records" string="My Records"
domain="[('user_id', '=', uid)]"/>
<filter name="this_month" string="This Month"
domain="[('date', '>=', context_today().replace(day=1))]"/>
<separator/>
<filter name="draft" string="Draft" domain="[('state', '=', 'draft')]"/>
<filter name="done" string="Done" domain="[('state', '=', 'done')]"/>
<group expand="0" string="Group By">
<filter name="group_partner" string="Partner" context="{'group_by': 'partner_id'}"/>
<filter name="group_state" string="State" context="{'group_by': 'state'}"/>
</group>
</search>
</field>
</record>
Top-level fields enable quick search; filters are pre-defined domains; the Group By group adds aggregation options.
View inheritance
The killer feature. Don't replace views — extend them:
<record id="view_partner_form_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit.my_module</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<!-- Add a field after email -->
<xpath expr="//field[@name='email']" position="after">
<field name="x_my_custom_field"/>
</xpath>
<!-- Replace a field's attributes -->
<xpath expr="//field[@name='phone']" position="attributes">
<attribute name="required">1</attribute>
</xpath>
<!-- Insert before a button -->
<xpath expr="//button[@name='action_archive']" position="before">
<button name="my_action" string="My Action" type="object"/>
</xpath>
<!-- Replace entire element -->
<xpath expr="//field[@name='website']" position="replace">
<field name="website" widget="url"/>
</xpath>
</field>
</record>
Position options: after, before, inside, replace, attributes.
Shortcut: attribute-based xpath
For simple field targeting, you can skip xpath:
<field name="email" position="after">
<field name="x_my_field"/>
</field>
Equivalent to <xpath expr="//field[@name='email']" position="after">. Cleaner for simple cases; xpath wins for complex selectors (multiple matches, ancestor traversal).
View modifiers
Show/hide/disable fields conditionally:
<!-- Odoo 17 and earlier: attrs -->
<field name="discount" attrs="{'invisible': [('state', '=', 'done')]}"/>
<field name="approver_id" attrs="{'required': [('amount', '>', 1000)]}"/>
<!-- Odoo 19: inline expressions (attrs is deprecated) -->
<field name="discount" invisible="state == 'done'"/>
<field name="approver_id" required="amount > 1000"/>
<field name="notes" readonly="state in ('done', 'cancel')"/>
Odoo 19's inline expressions are Python-syntax in attribute values. Easier to read, less verbose.
View priority
When multiple inherits target the same view, the one with lower priority applies first:
<record id="my_view" model="ir.ui.view">
<field name="priority">10</field> <!-- applies before priority 16 -->
...
</record>
Default priority is 16. Set lower numbers (e.g., 5, 8) for inherits that should apply early. Set higher (e.g., 20, 30) for inherits that should layer on top.
Common view debugging tips
| Problem | Likely cause |
|---|---|
| View change not visible | Module not upgraded; restart Odoo with -u my_module |
| xpath doesn't match | Parent view structure changed; inspect with developer mode |
| Field showing twice | Two inheriting views both add the same field |
| Field showing nowhere | xpath replaced it inadvertently |
| Inheritance order wrong | Adjust priority |
| Module load error | Malformed XML; check error log line number |
In developer mode, click "Edit View: Form" in the kebab menu to see the rendered XML and inspect inheritance chain.
View comparison
| View type | Use case | Inherits from |
|---|---|---|
| Form | Edit one record | base form view |
| List (tree) | Browse multiple records | base list |
| Kanban | Visual workflow | base kanban |
| Search | Filter and group | base search |
| Calendar | Time-based records | (standalone) |
| Gantt | Project schedules | (standalone) |
| Pivot | Aggregations | (standalone) |
| Graph | Charts | (standalone) |
Frequently Asked Questions
Should I use <tree> or <list> in Odoo 19?
Both work; <list> is the new canonical name. New code should use <list>. Existing code with <tree> still works and won't break — Odoo's view loader treats them as aliases. For consistency in a codebase, pick one and use it everywhere.
What's the difference between attrs and inline expressions in Odoo 19?
attrs is a dict syntax: attrs="{'invisible': [('state', '=', 'done')]}". Inline is Python: invisible="state == 'done'". Odoo 19 deprecated attrs in favor of inline because the latter is shorter and more readable. attrs still works but emits deprecation warnings; migrate when you next touch the file.
How do I add a button only for users in a specific group?
Use the groups attribute on the button: <button name="action_x" groups="my_module.group_admin"/>. The button renders only for users in that group. Alternative: use invisible="not user_has_group('my_module.group_admin')" for dynamic checks.
Can I have multiple search filters that combine differently?
Yes. Adjacent filters in the search view are OR'd within the same group. <separator/> creates a logical AND between groups. Filters with <group> wrapping share a group context (group-by behavior). Test combinations on real data — the logic surprises devs occasionally.
What's the right way to hide a column in a list inherited from another module?
Use <xpath position="attributes"> to set column_invisible="1" (only hides the column, doesn't remove the field), or position="replace" to remove it entirely. Removing breaks downstream views that may reference the field; setting invisible is safer.
XML views are the visible layer of Odoo customization. ECOSIRE's Odoo developer hire service places engineers who can navigate complex view inheritance chains and ship clean customizations. See our Odoo customization service for module development or browse our Odoo modules catalog for examples of well-structured XML views.
بقلم
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.
ECOSIRE
قم بتحويل أعمالك باستخدام Odoo ERP
تنفيذ وتخصيص ودعم خبير Odoo لتبسيط عملياتك.
مقالات ذات صلة
كيفية إضافة زر مخصص إلى عرض نموذج Odoo (2026)
إضافة أزرار إجراءات مخصصة إلى طرق عرض نموذج Odoo 19: طريقة إجراء Python، وعرض الميراث، والرؤية المشروطة، ومربعات حوار التأكيد. تم اختبار الإنتاج.
كيفية إضافة حقل مخصص في Odoo بدون الاستوديو (2026)
قم بإضافة حقول مخصصة عبر وحدة مخصصة في Odoo 19: وراثة النموذج، وامتداد العرض، والحقول المحسوبة، وقرارات المتجر/غير المتجر. الكود أولاً، يتم التحكم في الإصدار.
كيفية إضافة تقرير مخصص في أودو باستخدام التخطيط الخارجي
أنشئ تقرير PDF يحمل علامة تجارية في Odoo 19 باستخدام web.external_layout: قالب QWeb، تنسيق الورق، ربط الإجراء. مع طباعة الشعار + تجاوزات التذييل.