Skip to content

QWeb Templates

Basic Template Structure

Simple Template

xml
<template id="basic_template" name="Basic Template">
    <div class="container">
        <h1>Welcome to Our Site</h1>
        <p>This is a basic QWeb template.</p>
    </div>
</template>

Template with Variables

xml
<template id="user_greeting" name="User Greeting">
    <div class="greeting">
        <h2>Hello, <t t-esc="user_name"/>!</h2>
        <p>Today is <t t-esc="current_date"/></p>
    </div>
</template>

Website Template

xml
<template id="website_page" name="Website Page" page="True">
    <t t-call="website.layout">
        <div id="wrap" class="oe_structure oe_empty">
            <section class="s_banner">
                <div class="container">
                    <h1>Page Title</h1>
                    <p>Page content goes here</p>
                </div>
            </section>
        </div>
    </t>
</template>

QWeb Directives

t-esc (Escape HTML)

xml
<template id="escape_example">
    <div>
        <p>Safe text: <t t-esc="user_input"/></p>
        <p>User name: <span t-esc="user.name"/></p>
        <p>Formatted amount: <t t-esc="amount" t-options="{'widget': 'monetary'}"/></p>
    </div>
</template>

t-raw (Raw HTML)

xml
<template id="raw_html_example">
    <div>
        <div class="content" t-raw="html_content"/>
        <div class="description" t-raw="product.description_sale"/>
    </div>
</template>

t-if Conditional

xml
<template id="conditional_example">
    <div>
        <div t-if="user.is_admin" class="admin-panel">
            <h3>Admin Panel</h3>
            <p>Admin controls here</p>
        </div>
        
        <div t-if="not user.partner_id.is_company">
            <p>Individual customer content</p>
        </div>
        
        <div t-if="order.state == 'draft'" class="alert alert-info">
            <p>Order is in draft state</p>
        </div>
    </div>
</template>

t-elif and t-else

xml
<template id="multiple_conditions">
    <div class="status">
        <div t-if="order.state == 'draft'" class="badge badge-secondary">
            Draft
        </div>
        <div t-elif="order.state == 'confirmed'" class="badge badge-primary">
            Confirmed
        </div>
        <div t-elif="order.state == 'done'" class="badge badge-success">
            Completed
        </div>
        <div t-else="" class="badge badge-warning">
            Unknown Status
        </div>
    </div>
</template>

Loops and Iteration

t-foreach Basic Loop

xml
<template id="product_list">
    <div class="product-grid">
        <div t-foreach="products" t-as="product" class="product-card">
            <h4 t-esc="product.name"/>
            <p t-esc="product.description"/>
            <span class="price" t-esc="product.list_price"/>
        </div>
    </div>
</template>

Loop with Index

xml
<template id="numbered_list">
    <ol>
        <li t-foreach="items" t-as="item">
            <span class="index">Item #<t t-esc="item_index + 1"/>:</span>
            <span class="name" t-esc="item.name"/>
        </li>
    </ol>
</template>

Loop Variables

xml
<template id="loop_variables">
    <table class="table">
        <tr t-foreach="records" t-as="record" 
            t-att-class="'odd' if record_odd else 'even'">
            <td t-esc="record_index"/>
            <td t-esc="record.name"/>
            <td>
                <span t-if="record_first" class="badge">First</span>
                <span t-if="record_last" class="badge">Last</span>
            </td>
        </tr>
    </table>
</template>

Attributes and Dynamic Content

t-att- Dynamic Attributes

xml
<template id="dynamic_attributes">
    <div>
        <img t-att-src="product.image_url" 
             t-att-alt="product.name"
             t-att-class="'featured' if product.is_featured else 'regular'"/>
        
        <a t-att-href="'/product/' + slug(product)" 
           t-att-title="product.name">
            <t t-esc="product.name"/>
        </a>
        
        <input t-att-value="form_data.get('email', '')" 
               t-att-required="'required' if field_required else None"/>
    </div>
</template>

t-attf- Formatted Attributes

xml
<template id="formatted_attributes">
    <div>
        <div t-attf-class="product-card #{product.category.code} #{product.state}">
            <h3 t-esc="product.name"/>
        </div>
        
        <a t-attf-href="/shop/product/#{product.id}?category=#{category.id}">
            View Product
        </a>
        
        <div t-attf-style="color: #{product.color}; background: #{product.bg_color};">
            Styled content
        </div>
    </div>
</template>

t-att Dictionary Attributes

xml
<template id="dict_attributes">
    <div t-att="{'class': 'product-card ' + product.state,
                  'data-id': product.id,
                  'data-price': product.price}">
        <t t-esc="product.name"/>
    </div>
</template>

Template Inheritance

Template Extension

xml
<template id="extended_layout" inherit_id="website.layout">
    <xpath expr="//head" position="inside">
        <link rel="stylesheet" href="/custom_module/static/src/css/custom.css"/>
        <script src="/custom_module/static/src/js/custom.js"/>
    </xpath>
    
    <xpath expr="//header" position="after">
        <div class="custom-banner">
            <p>Custom banner content</p>
        </div>
    </xpath>
</template>

Replacing Content

xml
<template id="custom_footer" inherit_id="website.footer_custom">
    <xpath expr="//div[@class='container']" position="replace">
        <div class="container">
            <div class="row">
                <div class="col-md-6">
                    <h4>Contact Info</h4>
                    <p>Custom footer content</p>
                </div>
                <div class="col-md-6">
                    <h4>Follow Us</h4>
                    <p>Social media links</p>
                </div>
            </div>
        </div>
    </xpath>
</template>

Multiple Inheritance Points

xml
<template id="product_page_custom" inherit_id="website_sale.product">
    <xpath expr="//div[@id='product_details']" position="before">
        <div class="product-badges">
            <span t-if="product.is_new" class="badge badge-success">New</span>
            <span t-if="product.on_sale" class="badge badge-danger">Sale</span>
        </div>
    </xpath>
    
    <xpath expr="//div[@class='product-price']" position="after">
        <div class="product-savings" t-if="product.compare_list_price > product.list_price">
            <span class="savings">
                Save <t t-esc="product.compare_list_price - product.list_price"/>
            </span>
        </div>
    </xpath>
</template>

Advanced Directives

t-call Template Inclusion

xml
<template id="main_template">
    <div class="page-wrapper">
        <t t-call="custom_module.header_template"/>
        
        <main class="content">
            <t t-call="custom_module.breadcrumb_template">
                <t t-set="breadcrumb_items" t-value="page_breadcrumbs"/>
            </t>
            
            <div class="main-content">
                <t t-raw="0"/>
            </div>
        </main>
        
        <t t-call="custom_module.footer_template"/>
    </div>
</template>

t-set Variable Assignment

xml
<template id="variable_assignment">
    <div>
        <t t-set="currency" t-value="request.env.user.company_id.currency_id"/>
        <t t-set="is_mobile" t-value="request.httprequest.user_agent.is_mobile"/>
        <t t-set="current_year" t-value="datetime.datetime.now().year"/>
        
        <div t-att-class="'mobile-view' if is_mobile else 'desktop-view'">
            <p>Copyright <t t-esc="current_year"/> - All rights reserved</p>
            <p>Currency: <t t-esc="currency.name"/></p>
        </div>
    </div>
</template>

t-field Model Field Rendering

xml
<template id="model_fields">
    <div class="record-display">
        <h2 t-field="record.name"/>
        <div class="content" t-field="record.description"/>
        <div class="date" t-field="record.date" t-options="{'format': 'MMMM d, y'}"/>
        <div class="amount" t-field="record.amount" t-options="{'widget': 'monetary'}"/>
        <div class="image" t-field="record.image" t-options="{'widget': 'image'}"/>
    </div>
</template>

Forms and User Input

Basic Form

xml
<template id="contact_form">
    <form action="/contact/submit" method="post" class="s_website_form">
        <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
        
        <div class="form-group">
            <label for="name">Name *</label>
            <input type="text" class="form-control" name="name" required="True"/>
        </div>
        
        <div class="form-group">
            <label for="email">Email *</label>
            <input type="email" class="form-control" name="email" required="True"/>
        </div>
        
        <div class="form-group">
            <label for="message">Message</label>
            <textarea class="form-control" name="message" rows="5"/>
        </div>
        
        <button type="submit" class="btn btn-primary">Send Message</button>
    </form>
</template>

Form with Validation

xml
<template id="validated_form">
    <form action="/submit" method="post" class="s_website_form">
        <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
        
        <div class="form-group">
            <label for="email">Email</label>
            <input type="email" 
                   class="form-control" 
                   name="email" 
                   t-att-value="form_data.get('email', '')"
                   t-att-class="'is-invalid' if errors.get('email') else ''"/>
            <div t-if="errors.get('email')" class="invalid-feedback">
                <t t-esc="errors['email']"/>
            </div>
        </div>
        
        <div class="form-group">
            <label for="phone">Phone</label>
            <input type="tel" 
                   class="form-control" 
                   name="phone"
                   pattern="[0-9\-\+\s\(\)]*"
                   t-att-value="form_data.get('phone', '')"/>
        </div>
        
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
</template>

JavaScript Integration

Template with JavaScript

xml
<template id="interactive_template">
    <div class="interactive-component" data-component="custom-widget">
        <div class="content">
            <h3 t-esc="title"/>
            <div class="data" t-att-data-config="json.dumps(config)">
                <t t-foreach="items" t-as="item">
                    <div class="item" t-att-data-id="item.id">
                        <span t-esc="item.name"/>
                    </div>
                </t>
            </div>
        </div>
        
        <script type="text/javascript">
            document.addEventListener('DOMContentLoaded', function() {
                initCustomWidget();
            });
        </script>
    </div>
</template>

AJAX Template

xml
<template id="ajax_content">
    <div class="ajax-container" data-url="/api/data">
        <div class="loading" style="display: none;">
            <i class="fa fa-spinner fa-spin"/> Loading...
        </div>
        
        <div class="content" style="display: none;">
            <!-- Content loaded via AJAX -->
        </div>
        
        <div class="error" style="display: none;">
            <p class="text-danger">Error loading content</p>
        </div>
    </div>
</template>

E-commerce Templates

Product Card

xml
<template id="product_card_custom">
    <div class="product-card" t-att-data-product-id="product.id">
        <div class="product-image">
            <img t-att-src="website.image_url(product, 'image_1024')" 
                 t-att-alt="product.name"
                 class="img-fluid"/>
            
            <div class="product-badges" t-if="product.is_published">
                <span t-if="product.website_ribbon_id" 
                      class="badge"
                      t-att-style="'background-color: %s' % product.website_ribbon_id.bg_color"
                      t-esc="product.website_ribbon_id.name"/>
            </div>
        </div>
        
        <div class="product-info">
            <h5 class="product-title">
                <a t-att-href="product.website_url" t-esc="product.name"/>
            </h5>
            
            <div class="product-price">
                <span t-if="product.compare_list_price > product.list_price" 
                      class="text-muted text-decoration-line-through">
                    <t t-esc="product.compare_list_price" 
                       t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
                </span>
                
                <span class="current-price">
                    <t t-esc="product.list_price" 
                       t-options="{'widget': 'monetary', 'display_currency': website.currency_id}"/>
                </span>
            </div>
            
            <div class="product-actions">
                <a t-att-href="product.website_url" class="btn btn-primary btn-sm">
                    View Details
                </a>
            </div>
        </div>
    </div>
</template>

Shopping Cart Summary

xml
<template id="cart_summary_custom">
    <div class="cart-summary">
        <h4>Order Summary</h4>
        
        <div class="cart-items">
            <div t-foreach="website_sale_order.order_line" t-as="line" class="cart-item">
                <div class="item-info">
                    <span class="item-name" t-esc="line.name"/>
                    <span class="item-qty">Qty: <t t-esc="int(line.product_uom_qty)"/></span>
                </div>
                <div class="item-price">
                    <t t-esc="line.price_subtotal" 
                       t-options="{'widget': 'monetary', 'display_currency': website_sale_order.currency_id}"/>
                </div>
            </div>
        </div>
        
        <div class="cart-totals">
            <div class="subtotal">
                <span>Subtotal:</span>
                <span t-esc="website_sale_order.amount_untaxed" 
                      t-options="{'widget': 'monetary', 'display_currency': website_sale_order.currency_id}"/>
            </div>
            
            <div class="taxes" t-if="website_sale_order.amount_tax">
                <span>Taxes:</span>
                <span t-esc="website_sale_order.amount_tax" 
                      t-options="{'widget': 'monetary', 'display_currency': website_sale_order.currency_id}"/>
            </div>
            
            <div class="total">
                <strong>
                    <span>Total:</span>
                    <span t-esc="website_sale_order.amount_total" 
                          t-options="{'widget': 'monetary', 'display_currency': website_sale_order.currency_id}"/>
                </strong>
            </div>
        </div>
    </div>
</template>

Utility Functions

Helper Functions

xml
<template id="utility_examples">
    <div>
        <!-- Date formatting -->
        <p>Date: <t t-esc="datetime.datetime.now().strftime('%B %d, %Y')"/></p>
        
        <!-- URL building -->
        <a t-att-href="'/shop/category/%s' % slug(category)">
            <t t-esc="category.name"/>
        </a>
        
        <!-- Text manipulation -->
        <p t-esc="product.description[:100] + '...' if len(product.description) > 100 else product.description"/>
        
        <!-- Number formatting -->
        <span t-esc="'{:,.2f}'.format(product.list_price)"/>
        
        <!-- Boolean display -->
        <span t-esc="'Yes' if product.is_available else 'No'"/>
    </div>
</template>

Common Patterns

Responsive Layout

xml
<template id="responsive_layout">
    <div class="container-fluid">
        <div class="row">
            <div class="col-12 col-md-8">
                <main class="main-content">
                    <t t-raw="0"/>
                </main>
            </div>
            <div class="col-12 col-md-4">
                <aside class="sidebar">
                    <t t-call="custom_module.sidebar_content"/>
                </aside>
            </div>
        </div>
    </div>
</template>

Error Handling

xml
<template id="error_template">
    <div class="error-container">
        <t t-if="error_message">
            <div class="alert alert-danger">
                <h4>Error</h4>
                <p t-esc="error_message"/>
            </div>
        </t>
        
        <t t-if="not error_message and not data">
            <div class="alert alert-info">
                <p>No data available</p>
            </div>
        </t>
        
        <t t-if="data">
            <div class="content">
                <!-- Normal content -->
            </div>
        </t>
    </div>
</template>