Portal Templates: QWeb Pages With the Portal Layout
Portal pages are rendered with QWeb templates that inherit Odoo's standard portal layout — giving you the consistent header, breadcrumbs, and sidebar for free while you fill in the content.
<template id="portal_my_projects" name="My Projects">
<t t-call="portal.portal_layout"> <!-- reuse the standard shell -->
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-foreach="projects" t-as="project">
<div class="card"><t t-esc="project.name"/></div>
</t>
</t>
</template>
| Directive | Does |
|---|---|
t-call="portal.portal_layout" | wrap content in the portal shell |
t-foreach / t-as | loop over records the controller passed |
t-esc | output a value, HTML-escaped |
t-if | conditional content |
The key technique is t-call: instead of building a page from scratch, you call an existing layout template (portal.portal_layout or portal.portal_sidebar) and inject your content into it. This gives every portal page the same look, navigation, and responsive shell without duplication — and your pages automatically match Odoo's styling. Data comes from the controller: the variables you loop over (projects, etc.) are passed in the request.render(template, {values}) call — the template just displays them. Security in templates: use t-esc (escaped) for record data, not t-out/raw, unless the content is trusted — escaping prevents XSS from user-entered data appearing on the page. QWeb basics: t-foreach+t-as to loop, t-if/t-else to branch, t-att-* for dynamic attributes, t-field for formatted field values (dates, currency). Inherit-and-extend existing portal templates with xpath rather than copying them, so upgrades stay safe.
🏋️ Practical Exercise
- Create a portal QWeb template.
- Extend
portal.portal_layout. - List records in a table.
- Add breadcrumbs.
- Add pagination.
🔥 Challenge Exercise
Build a portal page template that extends the standard portal layout, lists the user’s documents in a paginated table, and adds breadcrumbs. Explain how portal templates reuse portal.portal_layout and portal.portal_table for a consistent look.
- How to use
portal.portal_layoutas a page wrapper - Listing page template with pager and sort controls
- Detail page template with breadcrumbs and status badges
t-fieldfor formatted output and share links with access tokens
Portal Template Structure
Portal templates live in your module under views/portal_templates.xml and are registered as ir.ui.view records with type="qweb". They are loaded by listing them in __manifest__.py under data.
Every portal page calls portal.portal_layout as its outer wrapper:
<template id="portal_my_loans" name="My Loans">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<!-- page content goes here -->
</t>
</template>
Adding to the Portal Home
Inherit portal.portal_my_home to add a link and counter to the /my/ home page:
<template id="portal_my_home_loan" name="Portal My Home: Loans"
inherit_id="portal.portal_my_home" priority="40">
<xpath expr="//div[hasclass('o_portal_docs')]" position="inside">
<t t-if="loan_count" t-call="portal.portal_docs_entry">
<t t-set="title">Loans</t>
<t t-set="url">/my/loans</t>
<t t-set="count" t-value="loan_count"/>
</t>
</xpath>
</template>
The loan_count variable is set by your controller's _prepare_home_portal_values method.
Listing Page Template
A full listing template with sort controls and pager:
<template id="portal_my_loans" name="My Loans">
<t t-call="portal.portal_layout">
<t t-set="breadcrumbs_searchbar" t-value="True"/>
<t t-call="portal.portal_searchbar">
<t t-set="title">My Loans</t>
<t t-set="searchbar_sortings" t-value="searchbar_sortings"/>
<t t-set="sortby" t-value="sortby"/>
</t>
<t t-if="not loans">
<div class="alert alert-info">You have no active loans.</div>
</t>
<t t-if="loans">
<div class="card">
<div class="table-responsive">
<table class="table table-sm o_portal_my_doc_table">
<thead>
<tr>
<th>Book</th>
<th>Loan Date</th>
<th>Return Date</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<t t-foreach="loans" t-as="loan">
<tr>
<td>
<a t-attf-href="/my/loans/#{loan.id}">
<t t-out="loan.book_id.name"/>
</a>
</td>
<td><t t-field="loan.loan_date"/></td>
<td><t t-field="loan.return_date"/></td>
<td>
<span t-attf-class="badge badge-#{
'success' if loan.state == 'returned' else
'danger' if loan.state == 'overdue' else 'info'
}">
<t t-out="loan.state"/>
</span>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="portal.pager"/>
</div>
</t>
</t>
</template>
Detail Page Template
The detail template shows one record and includes a share link using the access token:
<template id="portal_loan_detail" name="Loan Detail">
<t t-call="portal.portal_layout">
<t t-set="o_portal_fullwidth_alert">
<t t-call="portal.portal_back_in_edit_mode">
<t t-set="backend_url"
t-value="'/web#model=library.loan&id=%s&view_type=form' % loan.id"/>
</t>
</t>
<div class="row">
<div class="col-12 col-lg-8">
<div class="card">
<div class="card-header">
<h4>Loan: <t t-field="loan.book_id.name"/></h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-4">Member</dt>
<dd class="col-8"><t t-field="loan.member_id.name"/></dd>
<dt class="col-4">Loan Date</dt>
<dd class="col-8"><t t-field="loan.loan_date"/></dd>
<dt class="col-4">Return Date</dt>
<dd class="col-8"><t t-field="loan.return_date"/></dd>
</dl>
</div>
</div>
</div>
</div>
</t>
</template>
t-field vs t-out vs t-esc
| Directive | Use case | Escaping |
|---|---|---|
t-field | Odoo field — renders with proper formatting (dates, currencies, Many2one names) | Yes, via widget |
t-out | Any Python expression — renders HTML as-is | No (raw HTML) |
t-esc | Any Python expression — renders as escaped text | Yes |
Prefer t-field for model fields — it handles date formatting, currency symbols, and related record names automatically.
- All portal templates wrap content in
t-call="portal.portal_layout" - Add your section to
/my/by inheritingportal.portal_my_home - Use
t-call="portal.pager"for pagination - Use
t-fieldfor model fields — it formats dates, currencies, and relations correctly
Interview Questions
- How do portal templates stay consistent with Odoo’s portal look?
- What layout template do portal pages extend?
- How do you paginate portal lists?
- How do you add breadcrumbs?
- What is
portal.portal_table?
Related Topics
FAQ
In views/portal_templates.xml (or any XML file listed in __manifest__.py under data). They are ir.ui.view records with type="qweb" and no model attribute.
Add your assets to the web.assets_frontend bundle in __manifest__.py under assets. Frontend assets are loaded on all public-facing pages including the portal.
Yes. Bootstrap is included in Odoo's frontend assets, so all standard Bootstrap utility and component classes work in portal templates.

