- 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.
368 lines
11 KiB
Markdown
368 lines
11 KiB
Markdown
# 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](#attribute-expression-best-practices)
|
|
2. [None/Null Safety Patterns](#nonenull-safety-patterns)
|
|
3. [Variable Computation Patterns](#variable-computation-patterns)
|
|
4. [Common Pitfalls](#common-pitfalls)
|
|
5. [Real-World Examples](#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.
|
|
|
|
```xml
|
|
<!-- ❌ 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.
|
|
|
|
```xml
|
|
<!-- ✅ 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
|
|
# Python context
|
|
display_price = None
|
|
product_price = 100.0
|
|
```
|
|
|
|
```xml
|
|
<!-- ❌ 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
|
|
# Python context
|
|
product.uom_id = UoM(...) # Valid UoM with category_id
|
|
product.uom_id.category_id = None # Category is None
|
|
```
|
|
|
|
```xml
|
|
<!-- ❌ 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
|
|
# Python context
|
|
quantity = "invalid_string" # Should be int/float
|
|
```
|
|
|
|
```xml
|
|
<!-- ❌ 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:
|
|
|
|
```xml
|
|
<!-- ✅ 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`:
|
|
|
|
```xml
|
|
<!-- ✅ 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
|
|
|
|
```xml
|
|
<!-- ❌ 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`
|
|
|
|
```xml
|
|
<!-- ❌ 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
|
|
|
|
```python
|
|
# Context: product.uom_id might be None
|
|
```
|
|
|
|
```xml
|
|
<!-- ❌ 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
|
|
|
|
```xml
|
|
<!-- ❌ 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-*`
|
|
|
|
```xml
|
|
<!-- ❌ 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
|
|
|
|
```xml
|
|
<!-- ✅ 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
|
|
|
|
```xml
|
|
<!-- ✅ 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
|
|
|
|
```xml
|
|
<!-- ✅ 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
|
|
```bash
|
|
# Validate XML syntax
|
|
python3 -m xml.dom.minidom template.xml
|
|
|
|
# Or use pre-commit hooks
|
|
pre-commit run check-xml
|
|
```
|
|
|
|
### QWeb Template Testing
|
|
```python
|
|
# 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
|
|
```xml
|
|
<!-- 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
|
|
|
|
- [Odoo QWeb Documentation](https://www.odoo.com/documentation/18.0/developer/reference/frontend/qweb.html)
|
|
- [Odoo Templates](https://www.odoo.com/documentation/18.0/developer/reference/backend/orm.html#templates)
|
|
- [Python Ternary Expressions](https://docs.python.org/3/tutorial/controlflow.html#more-on-conditions)
|
|
|
|
---
|
|
|
|
## Related Issues & Fixes
|
|
|
|
- [website_sale_aplicoop Template Error Fix](./FIX_TEMPLATE_ERROR_SUMMARY.md) - Real-world example of this pattern
|
|
- [Git Commit 0a0cf5a](../../../.git/logs/HEAD) - Implementation of these patterns
|
|
|
|
---
|
|
|
|
**Last Updated**: 2026-02-16
|
|
**Odoo Version**: 18.0+
|
|
**Status**: ✅ Documented and tested
|