If you have been building custom modules in Odoo 17, you already know that one of the most powerful things you can do is override an existing view in Odoo 17 without touching the source code. That is the correct and safe way to do it, whether you are customizing a Sale Order form, adding a field to a Purchase Order, hiding a button in a contact form, or restructuring an entire notebook tab.
In this guide, I am going to walk you through everything you need to know about Odoo 17 view inheritance using XPath, from the basic structure of an inherited view to real-world examples covering all five XPath position types. By the end, you will be able to confidently override a view in Odoo 17 without ever modifying core code.
Table of Contents
What Is View Inheritance in Odoo 17?
View inheritance in Odoo 17 is the mechanism that allows you to extend or modify an existing view, such as a form, tree, kanban, or search view, by applying changes on top of it through a child view, without altering the original view’s XML.
Instead of rewriting the entire view, you create a new ir.ui.view record that references the parent view via inherit_id. Inside that record, you define <xpath> elements that tell Odoo exactly which node to target and what to do with it.
>> Official Odoo 17 View Architectures Reference
Think of it like a patch file: the parent view stays intact, and your child view applies modifications on top. When Odoo renders the view, it resolves the full inheritance chain and produces the final layout.
This design is at the heart of Odoo’s modular architecture. It is what allows dozens of modules to all modify the same Sale Order form view without ever conflicting, as long as each module uses inheritance correctly.
Why You Should Never Edit Core Views In Odoo
This is something every Odoo developer learns either from good training or the hard way: never modify odoo/addons/ source views directly.
Here is what happens when you do:
- Upgrades break your customization. When Odoo releases a patch or you upgrade to a minor version, the core file gets overwritten, and your changes disappear.
- Conflicts with other modules. Any other module that also inherits the same view will now face an inconsistent base, which causes XML parsing errors.
- Impossible to track changes. There is no audit trail. You will not know what you changed six months from now.
- Breaks the uninstall process. If you try to remove your custom module later, core views will be unexpected.
The correct approach, always, is to create a custom module and use inherit_id with XPath to override a view in odoo 17 to apply your changes cleanly on top.
How to Find the View ID You Want to Override
Before you can write your override a view in odoo 17 or an inherited view, you need the external ID of the parent view. Here are two reliable methods:
Method 1 – Using Developer Mode (Technical Menu)
- Enable Developer Mode in Odoo: go to Settings → General Settings → Developer Tools → Activate the developer mode.
- Navigate to Settings → Technical → User Interface → Views.
- Search for the model name (e.g.,
sale.order) or the view type. - Open the view. At the top, you will see the External ID (e.g.,
sale.view_order_form). That is what you use ininherit_id.
Method 2 – Inspect the View from the UI
With developer mode active, open any form or list view. Click the debug icon (bug icon) in the top bar and select “Edit View: Form” or “Edit View: List”. This opens the view XML directly and shows you the view’s external ID.
Structure of an Inherited View in Odoo 17
Every override a view in Odoo 17 follows the same base structure. Here is the complete skeleton:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="your_module.model_view_form_inherit" model="ir.ui.view">
<field name="name">model.view.form.inherit.your_module</field>
<field name="model">your.model</field>
<field name="inherit_id" ref="original_module.original_view_id"/>
<field name="arch" type="xml">
<!-- Your XPath expressions go here -->
<xpath expr="//field[@name='name']" position="after">
<field name="your_custom_field"/>
</xpath>
</field>
</record>
</odoo>XMLLet’s break down each field:
| Fields | Purpose |
| id | Unique XML ID for your inherited view record |
| name | Human-readable name — follows convention model.view.type.inherit.module |
| model | The Odoo model this view belongs to (must match the parent) |
| inherit_id | References the parent view using its external ID |
| arch type=”xml” | Contains your XPath inheritance specifications |
Odoo XPath Expressions – How to Target Any Element
The expr attribute in <xpath> uses standard XPath 1.0 syntax to locate nodes inside the parent view’s XML. Here are the most common patterns you will use every day:
>> W3C XPath 1.0 Specification
Target a field by name
<xpath expr="//field[@name='partner_id']" position="after">
Target a button by string label
<xpath expr="//button[@string='Confirm']" position="before">
Target a page (notebook tab) by name attribute
<xpath expr="//page[@name='order_lines']" position="inside">
Target a group by string
<xpath expr="//group[@string='Customer Information']" position="inside">
Target a div by class or name
<xpath expr="//div[@class='oe_title']" position="inside">
Target a deeply nested element
<xpath expr="//notebook/page[@name='general_info']/group/field[@name='email']" position="after">
Odoo’s hasclass() helper (for QWeb views)
<xpath expr="//div[hasclass('oe_kanban_card')]" position="inside">
Important: XPath in Odoo matches the first element it finds. If your expression matches more than one element or matches nothing, Odoo will throw an error. Always use specific attribute selectors like
[@name='...']or[@string='...']to be precise.
All Odoo XPath Position Values Explained with Examples
Odoo 17 supports six position values. Understanding each one is essential for effective view overriding.
position=”after”
Inserts the new content immediately after the matched element.
Use case: Add a new field after an existing one.
<xpath expr="//field[@name='partner_id']" position="after">
<field name="partner_ref"/>
</xpath>
Result: partner_ref appears directly below partner_id in the form.
position=”before”
Inserts the new content immediately before the matched element.
Use case: Add a warning label before a critical field.
<xpath expr="//field[@name='date_order']" position="before">
<div class="alert alert-warning" role="alert">
Please verify the order date before confirming.
</div>
</xpath>
position=”inside”
Inserts the new content inside the matched element, at the end.
Use case: Add new fields inside an existing group or page.
<xpath expr="//page[@name='other_info']" position="inside">
<group string="Custom Info">
<field name="custom_notes"/>
<field name="internal_ref"/>
</group>
</xpath>
Tip:
insideis the default position if you omit thepositionattribute entirely.
position=”replace”
Replaces the matched element entirely with new content. You can use $0 it as a placeholder to include the original element inside your replacement.
Use case: Replace a standard field widget with a custom one, or completely remove an element.
<!-- Replace a field with a customized version -->
<xpath expr="//field[@name='state']" position="replace">
<field name="state" widget="statusbar" statusbar_visible="draft,sent,sale,done"/>
</xpath>
<!-- Remove an element by replacing with nothing -->
<xpath expr="//button[@string='Preview']" position="replace"/>
Warning:
replaceis powerful but risky. If another module also inherits the same element, your replacement may break their inheritance chain. Use it only whenattributeswon’t do the job.
position=”attributes”
Modifies attributes of the matched element without changing the element itself. This is the safest position type and should be your first choice when you only need to hide, show, make readonly, or change a domain on an existing field.
The content inside the <xpath> is a set of <attribute> elements:
<!-- Make a field invisible -->
<xpath expr="//field[@name='discount']" position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
<!-- Make a field readonly -->
<xpath expr="//field[@name='price_unit']" position="attributes">
<attribute name="readonly">True</attribute>
</xpath>
<!-- Change a field's domain -->
<xpath expr="//field[@name='product_id']" position="attributes">
<attribute name="domain">[('sale_ok', '=', True), ('type', '!=', 'service')]</attribute>
</xpath>
<!-- Add a string attribute to a page tab -->
<xpath expr="//page[@name='order_lines']" position="attributes">
<attribute name="string">Products Ordered</attribute>
</xpath>
In Odoo 17 specifically: The
invisibleattribute now uses Python domain syntax with context variables. Instead of the oldattrsapproach, you can write:<attribute name="invisible">state not in ('draft', 'sent')</attribute>This is cleaner and more readable than the old
{"invisible": [("state", "not in", ["draft", "sent"])]}syntax.
position=”move”
Moves an existing element to a new location inside the view. You place position="move" on the content element inside the <xpath>, not on the <xpath> itself.
Use case: Reorder fields or move a button to a different place in the view.
<xpath expr="//field[@name='payment_term_id']" position="after">
<xpath expr="//field[@name='partner_ref']" position="move"/>
</xpath>
This moves the partner_ref field to appear after payment_term_id.
Real-World Examples
Here are four complete, production-ready view inheritance examples you can adapt directly and override a view in odoo 17.
Example 1 – Add a Custom Field to the Sale Order Form
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_sale_order_form_inherit_custom" model="ir.ui.view">
<field name="name">sale.order.form.inherit.custom</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!-- Add a custom internal notes field after the customer field -->
<xpath expr="//field[@name='partner_id']" position="after">
<field name="x_internal_notes" placeholder="Internal notes..."/>
</xpath>
</field>
</record>
</odoo>XMLExample 2 – Hide a Button on the Purchase Order Form
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_purchase_order_form_inherit_hide_btn" model="ir.ui.view">
<field name="name">purchase.order.form.inherit.hide.btn</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_rfq"/>
<field name="arch" type="xml">
<!-- Hide the "Send by Email" button for all users -->
<xpath expr="//button[@name='action_rfq_send']" position="attributes">
<attribute name="invisible">True</attribute>
</xpath>
</field>
</record>
</odoo>XMLExample 3 – Add a New Notebook Tab to the Contact (res.partner) Form
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_partner_form_inherit_custom_tab" model="ir.ui.view">
<field name="name">res.partner.form.inherit.custom.tab</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<!-- Add a new page at the end of the notebook -->
<xpath expr="//notebook" position="inside">
<page string="Custom Details" name="custom_details">
<group>
<field name="x_loyalty_points"/>
<field name="x_account_manager"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>XMLExample 4 – Add a Custom Column to the Product List View
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_template_tree_view_inherit_custom" model="ir.ui.view">
<field name="name">product.template.tree.inherit.custom</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_tree_view"/>
<field name="arch" type="xml">
<!-- Add a custom column after the product name -->
<xpath expr="//field[@name='name']" position="after">
<field name="x_supplier_sku" string="Supplier SKU"/>
</xpath>
</field>
</record>
</odoo>XMLShorthand: Overriding Without XPath Tags in Odoo
When you are targeting a single element by its field name, you can skip the <xpath> tag entirely and write directly:
<!-- Shorthand — same result as using xpath expr="//field[@name='description']" -->
<field name="description" position="after">
<field name="custom_field"/>
</field>XMLThis works for <field>, <button>, <group>, <page>, and similarly named elements. It is quicker to write but slightly less explicit. For complex, nested targets, always use <xpath> with a full expression.
Common Errors and How to Fix Them
| Error | Cause | Fix |
| Element ‘<xpath …>’ cannot be located in parent view | The expr doesn’t match any element | Enable debug mode, open the parent view XML, and verify the exact element name/attributes |
Invalid view definition at upgrade | Syntax error in your XML | Check for unclosed tags, missing quotes, or wrong attribute values |
| View inheritance failed | The inherit_id ref doesn’t exist | Confirm the module providing the parent view is in your dependencies (__manifest__.py → depends) |
| Element ‘<xpath …>’ cannot be located in the parent view | Duplicate field names in view | Make the expression more specific using parent element context: //page[@name='sale']/field[@name='state'] |
| Changes not visible after upgrade | Changes are not visible after the upgrade | Element ‘<xpath …>’ cannot be located in the parent view |
Best Practices for View Inheritance in Odoo 17
Following these practices will save you hours of debugging and make your modules maintainable long-term:
- Use
attributesoverreplacewhenever possible. Replacing an element breaks any other module that also targets that element. Modifying attributes leaves the element intact and is upgrade-safe. - Always add your module to
dependsin__manifest__.py. If you are inheritingsale.view_order_form, you must have'sale'in your dependencies. Missing this causesinherit_idto fail silently on some setups. - Name your view records consistently. Follow the Odoo convention:
model.view_type.inherit.your_module. Example:sale.order.form.inherit.blc_custom. This makes debugging much easier in the Views technical menu. - Use specific XPath selectors.
//field[@name='partner_id']is good.//fieldalone is dangerous — it matches the first<field>in the entire view regardless of what it is. - Use
priorityto control override order. If multiple modules inherit the same view and the order matters, set<field name="priority">16</field>(default is 16; lower values apply first). - Test inheritance with
--dev=xml. Run Odoo with--dev=xmlflag during development. This disables view caching and reloads XML on every request, so you see changes without restarting the server.
python3 odoo-bin -d your_db -u your_module --dev=xml
Conclusion
Overriding a view in Odoo 17 using XPath is one of the most important skills for any Odoo developer. It keeps your customizations upgrade-safe, module-isolated, and fully reversible. The key takeaways from this guide are:
- Always use
inherit_idand never modify core XML files directly. - Use
position="attributes"for hiding, making readonly, or changing field properties – it is the safest and most upgrade-compatible approach. - Use
position="after","before", and"inside"for adding new content. - Use
position="replace"Only when you need to completely restructure an element and nothing else will do. - Use –dev=xml during development, to skip view caching and see your changes in real time.
If you found this guide useful, check out related articles on:
>> How to add chatter in a custom form view,
>> How to add a systray icon in Odoo 17
>> How to add a button to the action menu in Odoo 17
>> Understanding OWL JS lifecycle hooks in Odoo 17.
Frequently Asked Questions
Q1. Can I override a view in Odoo 17 without creating a full custom module?
No. View inheritance in Odoo 17 requires a properly structured addon module with an __init__.py, __manifest__.py, and at least one XML file declaring the inherited view record. You cannot do this from the web interface alone (the Studio app in Enterprise edition is the exception, but it generates module files behind the scenes).
Q2. What is the difference between inherit_id and mode="primary"?
inherit_id points to the parent view. The default mode of an inherited view is extension that it applies on top of its parent. If you set mode="primary", the view becomes a standalone base view for a derived model — this is only needed in delegation inheritance scenarios where you have a model that extends another model (_inherits).
Q3. Can I inherit a view that is itself already an inherited view?
Yes. Odoo resolves the full inheritance chain. If Module B inherits a view from Module A, you can write Module C that inherits Module B’s extended view. Just point inherit_id to Module B’s view record. Be careful with this pattern, though — if Module A’s base view changes, the chain can break.
Q4. How do I find the correct name attribute value for a notebook page?
Enable Developer Mode, navigate to the view in the UI, click the debug icon (bug) → “Edit View: Form”. The full XML renders in a dialog. Search for <page and look at the name attribute. If no name is set, use @string to target it: //page[@string='Sales Information'].
Q5. What is the safest way to make a field conditionally invisible in Odoo 17?
Use position="attributes" with the new Python expression syntax introduced in Odoo 17:
<xpath expr=”//field[@name=’discount’]” position=”attributes”> <attribute name=”invisible”>not show_discount</attribute>
</xpath>
This is cleaner than the old attrs dictionary syntax and is the officially recommended approach in Odoo 17 onwards.
Q6. My XPath is correct, but it still fails — what should I check?
Check these three things in order: (1) Is the parent module listed in depends in your __manifest__.py? (2) Did you upgrade your module with -u your_module? (3) Is there another module with a lower priority that already uses replace the element you are targeting? The replace position removes the original element, which means your XPath targeting that element will find nothing.



