- 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
Frequently Asked Questions
Where do portal templates go in the module?
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.
How do I add custom CSS/JS to portal pages?
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.
Can I use Bootstrap classes in portal templates?
Yes. Bootstrap is included in Odoo's frontend assets, so all standard Bootstrap utility and component classes work in portal templates.