# XPath view inheritance (Odoo glossary)

Canonical: https://erpfly.com/glossary/xpath-view-inheritance/
Last updated: September 14, 2026

**Definition:** XPath view inheritance is how Odoo changes an existing view without editing it. A new ir.ui.view record points at the original through inherit_id, and xpath elements locate nodes in the parent arch to add content inside, before or after them, replace them, change their attributes or move them.

Also called: Odoo xpath, Odoo view inheritance, inherit_id, inherited view

A form in Odoo is never a single XML file. When the web client asks for the sale order form, the server takes the base view and applies every inheriting view from every installed module on top of it, then sends the result. Your customization is one more layer in that stack.

### How the final view gets built

Odoo resolves a view in a fixed order. It starts from the primary view's arch, then applies the inheriting views whose `mode` is `extension`, depth first: a child view, then that child's own children, then its siblings. When several views inherit the same parent, their `priority` decides the order.

A view with `mode` set to `primary` works differently. It takes the fully resolved parent as its starting point but becomes a separate view, so the original stays untouched for everyone else. That's the tool when you want a variant of a form for one action rather than a change to the form itself.

Inside a view, each spec finds its target with one of three locators: an `xpath` element with an `expr`, a `field` element matched by `name`, or any other element matched by tag and identical attributes. Every locator uses the first match only.

### The positions

`inside` is the default and appends to the matched node. `after` and `before` insert next to it. `replace` swaps it out, and a text node containing only `$0` inside your spec is replaced by the original node, which lets you wrap an element instead of deleting it. `attributes` sets or removes attributes. `move` takes an existing node from elsewhere in the view and puts it where your spec points.

### An example

This targets `sale.view_order_form`, where `commitment_date` already carries `readonly="state == 'cancel' or locked"` in 17, 18 and 19:

```xml
<record id="view_order_form_delivery_window" model="ir.ui.view">
    <field name="name">sale.order.form.delivery.window</field>
    <field name="model">sale.order</field>
    <field name="inherit_id" ref="sale.view_order_form"/>
    <field name="arch" type="xml">
        <xpath expr="//field[@name='commitment_date']" position="after">
            <field name="delivery_window"/>
            <field name="delivery_window_confirmed"/>
        </xpath>
        <!-- Odoo 18 and 19: combines with the existing expression -->
        <xpath expr="//field[@name='commitment_date']" position="attributes">
            <attribute name="readonly" add="delivery_window_confirmed" separator="or"/>
        </xpath>
        <xpath expr="//field[@name='delivery_window']" position="after">
            <field name="client_order_ref" position="move"/>
        </xpath>
    </field>
</record>
```

The `add` with `separator="or"` is worth knowing. Since Odoo 18, `add` and `remove` on `invisible`, `readonly`, `required`, `column_invisible` and `decoration-*` treat the value as a Python expression and only accept `and` or `or`, producing `(state == 'cancel' or locked) or (delivery_window_confirmed)`. Odoo 17 handles `add` and `remove` as plain string splitting on the separator, which mangles an expression, so on 17 you write the whole condition in the attribute body instead.

### Version traps

Odoo 17 dropped `attrs` and `states`, and an inherited view still using them fails validation. Odoo 18 renamed the root of list views from `tree` to `list`, so any xpath like `//field[@name='order_line']/tree` stops matching. Both errors are loud on install, which is better than silent. So is anchoring on a label: `//field[@string='Customer']` is rejected outright, because `string` is translated and Odoo won't use translated attributes as selectors. Matching on `@class` only logs a warning, and the fix it suggests is `hasclass()`.

The quieter problem is two addons that don't depend on each other targeting the same node. Priority settles the order, but if one of them replaces or moves that node, the other can fail with "cannot be located in parent view" on a database where both are installed. Our post on [_inherit vs _inherits](https://erpfly.com/blog/odoo-inherit-vs-inherits/) lists more of the xpath mistakes we catch in review.

erpfly writes view changes this way, anchored on field names, and the [Odoo customization](https://erpfly.com/odoo-customization/) page shows a larger inherited form. PDF reports use the same specs, since a report is a QWeb template stored as a view.

See also: https://erpfly.com/glossary/odoo-addon/, https://erpfly.com/glossary/qweb/, https://erpfly.com/glossary/odoo-studio/, https://erpfly.com/glossary/odoo-manifest/

### Sources

- [View records: Inheritance, Odoo 19 developer documentation](https://www.odoo.com/documentation/19.0/developer/reference/user_interface/view_records.html)
- [View architectures, Odoo 19 developer documentation](https://www.odoo.com/documentation/19.0/developer/reference/user_interface/view_architectures.html)
- [odoo/tools/template_inheritance.py (18.0), odoo/odoo on GitHub](https://github.com/odoo/odoo/blob/18.0/odoo/tools/template_inheritance.py)