Quick Answer
Basic <xpath> inheritance can't handle complex conditionals, loops, or dynamic filtering. Developers resort to Python workarounds and JavaScript patches—costing $18,000-$50,000 in unnecessary complexity. The fix: Master 8 advanced XML techniques (QWeb conditionals, loops, dynamic domains, attribute manipulation). Build sophisticated views in XML alone.
The XML Complexity Problem
You're building a custom sales order view. You need to:
❏ Show a "Urgent" badge only if order is high priority AND placed more than 3 days ago
❏ Hide payment method field if order status is "draft"
❏ Loop through line items and show only items over $100
❏ Dynamically filter available customers based on sales rep's assigned territory
❏ Color-code rows based on multiple conditions
This is where basic XML inheritance breaks down. Simple <xpath> statements can't handle this complexity. You need conditional logic, loops, dynamic filtering, attribute manipulation.
We've implemented 150+ Odoo systems. The developers who master advanced XML? They build sophisticated, flexible views that adapt to complex business rules. The ones who don't? They resort to Python workarounds, custom CSS hacks, and JavaScript patches. That's $18,000-$50,000 in unnecessary complexity and maintenance debt.
Technique 1: QWeb Conditionals (t-if, t-elif, t-else)
The Problem: You need to show different content based on conditions.
Simple Example: Show Status Badge
<record id="view_order_form" model="ir.ui.view">
<field name="name">Sale Order Form</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- Show badge based on state -->
<div class="oe_right">
<t t-if="record.state == 'draft'">
<span class="badge badge-warning">Draft</span>
</t>
<t t-elif="record.state == 'done'">
<span class="badge badge-success">Completed</span>
</t>
<t t-else="">
<span class="badge badge-info">In Progress</span>
</t>
</div>
</sheet>
</form>
</field>
</record>
Real D2C Example: Complex Conditional Logic
<!-- Show "URGENT" badge only if:
1. Priority is "high"
2. AND order is NOT yet shipped
3. AND created more than 3 days ago
-->
<div class="alert alert-danger"
t-if="record.x_priority == 'high' and record.state != 'done'">
<strong>🚨 URGENT ORDER</strong>
<p>This order requires immediate attention</p>
</div>
<div class="alert alert-warning"
t-elif="record.amount_total > 5000 and record.state == 'draft'">
<strong>⚠️ HIGH VALUE - NEEDS APPROVAL</strong>
<p>This order exceeds $5,000 and needs manager approval</p>
</div>
<div class="alert alert-info"
t-elif="record.customer_id.x_loyalty_tier == 'gold'">
<strong>👑 VIP CUSTOMER</strong>
<p>Provide priority service for this valued customer</p>
</div>
Key Conditional Operators
<!-- EQUALITY -->
<t t-if="record.state == 'draft'">Draft</t>
<!-- INEQUALITY -->
<t t-if="record.state != 'done'">Not Done</t>
<!-- COMPARISON -->
<t t-if="record.amount_total > 1000">High Value</t>
<t t-if="record.quantity_delivered >= quantity">All Delivered</t>
<!-- BOOLEAN -->
<t t-if="record.x_is_priority">Priority Order</t>
<t t-if="not record.x_is_priority">Regular Order</t>
<!-- LOGICAL AND -->
<t t-if="record.state == 'sale' and record.amount_total > 500">
High Value Sale
</t>
<!-- LOGICAL OR -->
<t t-if="record.state == 'draft' or record.state == 'sent'">
Pending Order
</t>
<!-- CONTAINS (for text fields) -->
<t t-if="'urgent' in record.name.lower()">Urgent</t>
<!-- IN (for selection fields) -->
<t t-if="record.state in ['draft', 'sent']">Not Confirmed</t>
<!-- LIST OPERATIONS -->
<t t-if="len(record.order_line) > 0">Has Items</t>
<t t-if="any(line.price_total > 100 for line in record.order_line)">
Has expensive items
</t>
Technique 2: Loops (t-foreach)
The Problem: You need to iterate through related records and show data from each.
Example: Loop Through Order Lines
<table class="table">
<thead>
<tr>
<th>Product</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<!-- t-foreach loops through each order line
t-as names the variable for each iteration
t-key is unique identifier for performance
-->
<tr t-foreach="record.order_line" t-as="line" t-key="line.id">
<td>
<t t-esc="line.product_id.name"/>
</td>
<td>
<t t-esc="line.product_qty"/>
</td>
<td>
<t t-esc="line.price_unit"/>
</td>
<td>
<t t-esc="line.price_total"/>
</td>
<td>
<!-- CONDITIONAL INSIDE LOOP -->
<t t-if="line.qty_delivered == line.product_qty">
<span class="badge badge-success">Delivered</span>
</t>
<t t-elif="line.qty_delivered > 0">
<span class="badge badge-info">Partial</span>
</t>
<t t-else="">
<span class="badge badge-secondary">Pending</span>
</t>
</td>
</tr>
</tbody>
</table>
Loop Variables Available
<div t-foreach="record.order_line" t-as="line">
<!-- Current item -->
<t t-esc="line.product_id.name"/>
<!-- Index (0, 1, 2, ...) -->
<t t-esc="line_index"/>
<!-- Is first iteration? -->
<t t-if="line_first">FIRST</t>
<!-- Is last iteration? -->
<t t-if="line_last">LAST</t>
<!-- Counter (1, 2, 3, ...) -->
<t t-esc="line_counter"/>
<!-- Is odd iteration? -->
<t t-if="line_parity == 'odd'">Odd</t>
<!-- Is even iteration? -->
<t t-if="line_parity == 'even'">Even</t>
</div>
Real D2C Example: Show Only Expensive Items
<!-- Show only items over $100, with alternating row colors -->
<table>
<tr t-foreach="record.order_line" t-as="line" t-key="line.id"
t-attf-class="{{'table-warning': line_parity == 'even'}}"
t-if="line.price_total > 100">
<td><t t-esc="line_counter"/></td>
<td><t t-esc="line.product_id.name"/></td>
<td><t t-esc="line.price_total" t-options="{'widget': 'monetary'}"/></td>
</tr>
</table>
Technique 3: Dynamic Attributes (t-att, t-attf)
The Problem: You need to set HTML attributes dynamically based on field values.
Example: Color-Coded Rows Based on Status
<!-- t-att = set attribute from expression -->
<tr t-foreach="record.order_line" t-as="line" t-key="line.id"
t-att-class="'table-danger' if line.qty_delivered == 0 else 'table-success'">
<td><t t-esc="line.product_id.name"/></td>
</tr>
<!-- t-attf = format string for attributes -->
<div t-foreach="record.tags" t-as="tag" t-key="tag.id"
t-attf-class="badge badge-{{ tag.color }}">
<t t-esc="tag.name"/>
</div>
More Complex Example: Set Data Attributes
<div t-foreach="record.order_line" t-as="line" t-key="line.id"
t-att-data-price="line.price_total"
t-att-data-product-id="line.product_id.id"
t-att-data-status="line.state"
class="order-item">
<t t-esc="line.product_id.name"/>
</div>
<!-- Result HTML -->
<!-- <div data-price="1500" data-product-id="25" data-status="done" class="order-item">
Product Name
</div>
-->
Technique 4: Dynamic Domains (Filter Dropdown Options)
The Problem: You need to filter Many2One dropdown options based on another field's value.
Example: Filter Customers by Sales Rep's Territory
<record id="view_order_form" model="ir.ui.view">
<field name="name">Sale Order Form</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<!-- Sales rep field -->
<field name="user_id"/>
<!-- Customer field - filtered by rep's territory -->
<field name="partner_id"
domain="[('territory_id', '=', user_id.territory_id)]"
context="{'default_user_id': user_id}"/>
</group>
</sheet>
</form>
</field>
</record>
Real D2C Example: Complex Filtering
<!-- Filter available products by:
1. Category matches order's product category
2. Supplier matches order's preferred supplier
3. Stock > 0
-->
<field name="product_id"
domain="[
('category_id', '=', x_product_category),
('supplier_ids.name', '=', x_preferred_supplier),
('qty_available', '>', 0)
]"/>
<!-- Filter available shipping methods by:
1. Destination country matches customer's country
2. Weight <= method's max weight
-->
<field name="shipping_method_id"
domain="[
('dest_country_id', '=', partner_id.country_id),
('max_weight', '>=', weight_total)
]"/>
Technique 5: XPath Position="attributes" (Modify Element Attributes)
The Problem: You need to change attributes of existing elements without replacing them.
<record id="view_order_form_extended" model="ir.ui.view">
<field name="name">Order Form Extended</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<!-- Make email field required -->
<xpath expr="//field[@name='partner_id']" position="attributes">
<attribute name="required">true</attribute>
<attribute name="readonly">false</attribute>
</xpath>
<!-- Add domain to product field -->
<xpath expr="//field[@name='product_id']" position="attributes">
<attribute name="domain">[('active', '=', True)]</attribute>
</xpath>
<!-- Add widget to date field -->
<xpath expr="//field[@name='date_order']" position="attributes">
<attribute name="widget">date</attribute>
</xpath>
</field>
</record>
Technique 6: Setting Variables (t-set)
The Problem: You need to compute a value once and reuse it multiple times.
<div>
<!-- SET a variable -->
<t t-set="is_high_priority" t-value="record.x_priority == 'high'"/>
<!-- USE the variable -->
<div t-if="is_high_priority" class="alert alert-danger">
This order is high priority
</div>
<button t-if="is_high_priority" class="btn btn-danger">
Mark Urgent
</button>
</div>
<!-- Computed value example -->
<t t-set="total_delivered"
t-value="sum(line.qty_delivered for line in record.order_line)"/>
<p>
Total Delivered: <t t-esc="total_delivered"/> / <t t-esc="record.amount_total"/>
</p>
Real D2C Example: Calculate and Display Metrics
<!-- Calculate metrics once, use multiple times -->
<t t-set="total_items"
t-value="sum(line.product_qty for line in record.order_line)"/>
<t t-set="avg_price"
t-value="record.amount_total / total_items if total_items > 0 else 0"/>
<t t-set="is_large_order"
t-value="total_items >= 50"/>
<div class="order-summary">
<p>Total Items: <t t-esc="total_items"/></p>
<p>Average Price: $<t t-esc="avg_price"/></p>
<t t-if="is_large_order">
<p class="text-warning">⚠️ This is a large order, prioritize packing</p>
</t>
</div>
Technique 7: Context Variables (Using Python Context)
The Problem: You need to use values from Python context in your XML view.
Python Setup: Add Context Variables
# In your Python model
def get_view(self, view_id=None, view_type='form', **options):
"""Override to add context variables."""
result = super().get_view(view_id, view_type, **options)
if view_type == 'form':
# Add custom context variable
result['context'] = {
'is_admin': self.env.user.has_group('base.group_erp_manager'),
'current_user_territory': self.env.user.partner_id.territory_id.id,
'today': fields.Date.today(),
}
return result
XML Usage: Use Context in Views
<record id="view_order_form" model="ir.ui.view">
<field name="name">Sale Order Form</field>
<field name="model">sale.order</field>
<field name="arch" type="xml">
<form>
<sheet>
<!-- Show admin-only section -->
<t t-if="is_admin">
<group string="Admin Section">
<field name="x_internal_cost"/>
<field name="x_profit_margin"/>
</group>
</t>
<!-- Show delivery address if in same territory -->
<t t-if="partner_id.territory_id == current_user_territory">
<group string="Delivery Address">
<field name="partner_shipping_id"/>
</group>
</t>
</sheet>
</form>
</field>
</record>
Technique 8: Output Formatting (t-esc, t-out, t-field)
Different ways to output data:
<!-- t-esc = Escape HTML (safe for text) -->
<p><t t-esc="record.name"/></p>
<!-- Result: <p>Order #12345</p> (HTML safe) -->
<!-- t-out = Output HTML (not escaped, dangerous!) -->
<p><t t-out="record.description_html"/></p>
<!-- Result: <p><strong>Bold</strong> text</p> (HTML rendered) -->
<!-- t-field = Format field with widget -->
<p><t t-field="record.price" t-options="{'widget': 'monetary'}"/></p>
<!-- Result: <p>$1,500.00</p> (formatted with currency) -->
<!-- Combining with format -->
<p>
Order: <t t-esc="record.name"/>
Amount: <t t-field="record.amount_total" t-options="{'widget': 'monetary'}"/>
</p>
Techniques Comparison
| Technique | Use Case | Example |
|---|---|---|
| QWeb Conditionals | Show/hide content based on conditions | t-if="state == 'draft'" |
| Loops | Iterate through related records | t-foreach="order_line" |
| Dynamic Attributes | Set HTML attributes dynamically | t-att-class="'danger'" |
| Dynamic Domains | Filter dropdown options | domain="[('active', '=', True)]" |
| XPath Attributes | Modify existing element attributes | position="attributes" |
| Variables | Compute once, reuse multiple times | t-set="is_urgent" |
| Context Variables | Use Python context in XML | t-if="is_admin" |
| Output Formatting | Format field values | t-field, t-esc, t-out |
Action Items: Master These Techniques
Master These Techniques
❏ Use t-if/t-elif/t-else for conditional display
❏ Use t-foreach for loops with line_index, line_first, line_last
❏ Use t-att/t-attf for dynamic HTML attributes
❏ Use domain attribute for dynamic dropdown filtering
❏ Use xpath position="attributes" for modifying existing elements
❏ Use t-set for computed variables
❏ Use t-field for formatted output
For Complex Views
✓ Break complex conditionals into multiple simpler ones
✓ Use t-set to compute expensive calculations once
✓ Use meaningful variable names (is_urgent vs is_u)
✓ Test extensively in development before deploying
Frequently Asked Questions
When should I use t-if vs Python computed fields?
Use t-if for view-only display logic (showing/hiding UI elements). Use Python computed fields when you need the value for business logic, searches, or reports. t-if is faster for simple display conditions.
How do I debug QWeb conditional expressions in Odoo?
Enable Developer Mode, then use <t t-esc="variable_name"/> to output values. Check browser console for JavaScript errors. Use t-set to break complex expressions into smaller testable pieces.
Can I use Python list comprehensions in QWeb t-foreach?
No. Use t-foreach with t-if inside the loop to filter. Example: <tr t-foreach="lines" t-as="line" t-if="line.price > 100">. For complex filtering, use computed fields in Python.
How much can I save by mastering advanced XML vs. Python workarounds?
Developers who master XML avoid $18,000-$50,000 in Python workarounds, JavaScript patches, and maintenance debt. XML is declarative, easier to maintain, and doesn't require custom module updates when Odoo upgrades.
Free Advanced XML Customization Workshop
Stop struggling with complex Odoo views. We'll teach you every advanced XML technique, show you real-world D2C examples, help you solve your specific view challenges, and review your custom XML for performance and correctness. Most D2C brands discover they're building views the hard way (with Python hacks) when XML could do it perfectly. Learning advanced XML saves $20,000-$50,000 in unnecessary Python code and complexity.
