addons-cm/docs/QWEB_BEST_PRACTICES.md
snt 6fed8639ed [DOC] Add QWeb template best practices and error fix documentation
- FIX_TEMPLATE_ERROR_SUMMARY.md: Complete analysis of the website_sale_aplicoop template error and its resolution
  * Root cause: QWeb parsing issues with 'or' operators in t-attf-* attributes
  * Solution: Pre-compute safe variables using t-set before form element
  * Verification: Template loads successfully, variables render correctly
  * Git commits: df57233 (first attempt), 0a0cf5a (final fix)

- QWEB_BEST_PRACTICES.md: Comprehensive guide for QWeb template development
  * Attribute expression best practices
  * None/null safety patterns (3 patterns with examples)
  * Variable computation patterns (3 patterns with examples)
  * Common pitfalls and solutions
  * Real-world examples (e-commerce, nested data, conditional styling)
  * Summary table and validation tools

These documents provide immediate reference for QWeb issues and establish
standards for template development in Odoo 18 projects.
2026-02-16 23:10:39 +01:00

11 KiB

QWeb Template Best Practices - Odoo 18

Reference: website_sale_aplicoop template error fix Odoo Version: 18.0+ Created: 2026-02-16


Table of Contents

  1. Attribute Expression Best Practices
  2. None/Null Safety Patterns
  3. Variable Computation Patterns
  4. Common Pitfalls
  5. Real-World Examples

Attribute Expression Best Practices

The Problem: t-attf-* Operator Issues

Issue: QWeb's t-attf-* (template attribute) directives don't handle chained or operators well when expressions can evaluate to None.

<!-- ❌ PROBLEMATIC -->
<form t-attf-data-price="{{ price1 or price2 or 0 }}">

<!-- Error when price1 is None and QWeb tries to evaluate: 'NoneType' object is not callable -->

The Solution: Pre-compute Safe Variables

Key Pattern: Use <t t-set> to compute safe values before using them in attributes.

<!-- ✅ CORRECT -->
<t t-set="safe_price"
   t-value="price1 if price1 else (price2 if price2 else 0)"/>
<form t-attf-data-price="{{ safe_price }}">

Why This Works

  1. Separation of Concerns: Logic (t-set) is separate from rendering (t-attf-*)
  2. Explicit Evaluation: QWeb evaluates the conditional expression fully before passing to t-set
  3. Type Safety: Pre-computed value is guaranteed to be non-None
  4. Readability: Clear intent of what value is being used

None/Null Safety Patterns

Pattern 1: Simple Fallback

Scenario: Value might be None, need a default

# Python context
display_price = None
product_price = 100.0
<!-- ❌ BAD: Inline or operator in attribute -->
<div t-attf-data-price="{{ display_price or product_price or 0 }}"/>

<!-- ✅ GOOD: Pre-computed safe variable -->
<t t-set="safe_price"
   t-value="display_price if display_price else (product_price if product_price else 0)"/>
<div t-attf-data-price="{{ safe_price }}"/>

Pattern 2: Nested Object Access

Scenario: Need to access nested attributes safely (e.g., product.uom_id.category_id.name)

# Python context
product.uom_id = UoM(...)        # Valid UoM with category_id
product.uom_id.category_id = None # Category is None
<!-- ❌ BAD: Will crash if category_id is None -->
<div t-attf-data-category="{{ product.uom_id.category_id.name }}"/>

<!-- ❌ BAD: Inline ternary in attribute (parsing issues) -->
<div t-attf-data-category="{{ product.uom_id.category_id.name if product.uom_id.category_id else '' }}"/>

<!-- ✅ GOOD: Pre-compute with null-safe chaining -->
<t t-set="safe_category"
   t-value="product.uom_id.category_id.name if (product.uom_id and product.uom_id.category_id) else ''"/>
<div t-attf-data-category="{{ safe_category }}"/>

Pattern 3: Type Coercion

Scenario: Value might be wrong type, need guaranteed type

# Python context
quantity = "invalid_string"  # Should be int/float
<!-- ❌ BAD: Type mismatch in attribute -->
<input t-attf-value="{{ quantity }}"/>

<!-- ✅ GOOD: Pre-compute with type checking -->
<t t-set="safe_qty"
   t-value="int(quantity) if (quantity and str(quantity).isdigit()) else 0"/>
<input t-attf-value="{{ safe_qty }}"/>

Variable Computation Patterns

Pattern 1: Sequential Computation

When multiple safe variables depend on each other:

<!-- ✅ GOOD: Order matters, compute dependencies first -->
<t t-set="has_category" t-value="product.uom_id and product.uom_id.category_id"/>
<t t-set="category_name" t-value="product.uom_id.category_id.name if has_category else 'Uncategorized'"/>
<t t-set="display_text" t-value="category_name.upper() if category_name else 'NONE'"/>

<span t-text="display_text"/>

Pattern 2: Conditional Blocks with t-set

When logic is complex, use t-if with t-set:

<!-- ✅ GOOD: Complex logic in t-if with t-set fallback -->
<t t-if="product.special_price">
    <t t-set="final_price" t-value="product.special_price"/>
</t>
<t t-else="">
    <t t-set="final_price" t-value="product.list_price or 0"/>
</t>

<div t-attf-data-price="{{ final_price }}"/>

Pattern 3: Python Expressions vs. Template Expressions

<!-- ❌ BAD: Complex Python in template (hard to read) -->
<div t-text="', '.join([p.name for p in products if p.active and p.price > 0])"/>

<!-- ✅ GOOD: Compute in Python model/controller, reference in template -->
<t t-set="active_products" t-value="products.filtered('active').filtered(lambda p: p.price > 0)"/>
<div t-text="', '.join(active_products.mapped('name'))"/>

Common Pitfalls

Pitfall 1: Trusting or in Attributes

Problem: The or operator in template attributes doesn't work like Python's or

<!-- ❌ WRONG: Looks right, but QWeb doesn't handle it correctly -->
<div t-attf-title="{{ obj.title or 'Default Title' }}"/>

<!-- ✅ CORRECT: Use explicit t-set -->
<t t-set="title" t-value="obj.title or 'Default Title'"/>
<div t-attf-title="{{ title }}"/>

Why: QWeb's attribute template system has special parsing rules that don't work well with complex expressions.

Pitfall 2: Chained Attribute Access Without Null-Checking

Problem: Assuming nested attributes exist

# Context: product.uom_id might be None
<!-- ❌ WRONG: Will crash if uom_id is None -->
<div t-attf-uom="{{ product.uom_id.category_id.name }}"/>

<!-- ✅ CORRECT: Check entire chain -->
<t t-set="uom_cat"
   t-value="product.uom_id.category_id.name if (product.uom_id and product.uom_id.category_id) else ''"/>
<div t-attf-uom="{{ uom_cat }}"/>

Pitfall 3: Complex Logic in t-att (non-template attributes)

Problem: Using complex expressions in non-template attributes

<!-- ❌ WRONG: Complex expression in regular attribute -->
<div data-value="{{ complex_function(arg1, arg2) if condition else default }}"/>

<!-- ✅ CORRECT: Pre-compute, keep attributes simple -->
<t t-set="computed_value" t-value="complex_function(arg1, arg2) if condition else default"/>
<div data-value="{{ computed_value }}"/>

Pitfall 4: Forgetting t-attf- Prefix

Problem: Using data-* instead of t-attf-data-*

<!-- ❌ WRONG: Not interpreted as template attribute -->
<form data-product-id="{{ product.id }}"/>
<!-- Result: Literal "{{ product.id }}" in HTML, not rendered -->

<!-- ✅ CORRECT: Use t-attf- prefix for template attributes -->
<form t-attf-data-product-id="{{ product.id }}"/>
<!-- Result: Actual product ID in HTML -->

Real-World Examples

Example 1: E-commerce Product Card

Scenario: Displaying product with optional fields

<!-- ✅ GOOD: Handles None prices, missing categories -->

<!-- Compute safe values first -->
<t t-set="display_price"
   t-value="product.special_price if product.special_price else product.list_price"/>
<t t-set="safe_price"
   t-value="display_price if display_price else 0"/>
<t t-set="has_tax"
   t-value="product.taxes_id and len(product.taxes_id) > 0"/>
<t t-set="price_with_tax"
   t-value="safe_price * (1 + (product.taxes_id[0].amount/100 if has_tax else 0))"/>

<!-- Use pre-computed values in form -->
<form class="product-card"
    t-attf-data-product-id="{{ product.id }}"
    t-attf-data-price="{{ safe_price }}"
    t-attf-data-price-with-tax="{{ price_with_tax }}"
    t-attf-data-has-tax="{{ '1' if has_tax else '0' }}"
>
    <input type="hidden" name="product_id" t-attf-value="{{ product.id }}"/>
    <span class="price" t-text="'{:.2f}'.format(safe_price)"/>
</form>

Example 2: Nested Data Attributes

Scenario: Form with deeply nested object access

<!-- ✅ GOOD: Null-safe navigation for nested objects -->

<!-- Define safe variables for nested chains -->
<t t-set="partner_id" t-value="order.partner_id.id if order.partner_id else ''"/>
<t t-set="partner_name" t-value="order.partner_id.name if order.partner_id else 'N/A'"/>
<t t-set="company_name"
   t-value="order.partner_id.company_id.name if (order.partner_id and order.partner_id.company_id) else 'N/A'"/>
<t t-set="address"
   t-value="order.partner_id.street if order.partner_id else 'No address'"/>

<!-- Use in form attributes -->
<form class="order-form"
    t-attf-data-partner-id="{{ partner_id }}"
    t-attf-data-partner-name="{{ partner_name }}"
    t-attf-data-company="{{ company_name }}"
    t-attf-data-address="{{ address }}"
>
    ...
</form>

Example 3: Conditional Styling

Scenario: Attribute value depends on conditions

<!-- ✅ GOOD: Pre-compute class/style values -->

<t t-set="stock_level" t-value="product.qty_available"/>
<t t-set="is_low_stock" t-value="stock_level and stock_level <= 10"/>
<t t-set="css_class"
   t-value="'product-low-stock' if is_low_stock else 'product-in-stock'"/>
<t t-set="disabled_attr"
   t-value="'disabled' if (stock_level == 0) else ''"/>

<div t-attf-class="product-card {{ css_class }}"
     t-attf-data-stock="{{ stock_level }}"
     t-attf-disabled="{{ disabled_attr if disabled_attr else None }}">
    ...
</div>

Summary Table

Pattern Don't Do
Fallback values t-attf-x="{{ a or b or c }}" <t t-set="x" t-value="a or b or c"/> then {{ x }}
Nested objects {{ obj.nested.prop }} <t t-set="val" t-value="obj.nested.prop if (obj and obj.nested) else ''"/>
Type checking <input value="{{ qty }}"/> <t t-set="safe_qty" t-value="int(qty) if is_digit(qty) else 0"/>
Complex logic {{ function(a, b) if condition else default }} Pre-compute in Python, reference in template
Chained operators {{ a or b if c else d or e }} Break into multiple t-set statements

Tools & Validation

XML Validation

# Validate XML syntax
python3 -m xml.dom.minidom template.xml

# Or use pre-commit hooks
pre-commit run check-xml

QWeb Template Testing

# In Odoo shell
from odoo.tools import misc
arch = env['ir.ui.view'].search([('name', '=', 'template_name')])[0].arch
# Check if template compiles without errors

Debugging Template Issues

<!-- Add debug output -->
<t t-set="debug_info" t-value="'DEBUG: value=' + str(some_value)"/>
<span t-if="debug_mode" t-text="debug_info"/>

<!-- Use JavaScript console -->
<script>
    console.log('Data attributes:', document.querySelector('.product-card').dataset);
</script>

References



Last Updated: 2026-02-16 Odoo Version: 18.0+ Status: Documented and tested