第十三章 QWeb 模板
篇别:第一篇 基础知识
本章学习目标
- 熟练使用 t-if / t-foreach / t-set / t-call / t-att 等指令编写可维护模板。
- 能正确选择 t-out / t-esc 与 t-raw,从设计上规避 XSS。
- 理解 子模板复用 与 变量作用域(
t-set在t-call中的传参)。 - 能区分 Python 端 QWeb(报表、邮件)与 JS 端 的差异,并指向对应文档。
导读:QWeb 是什么
QWeb 是 Odoo 的 XML 模板语言,广泛用于:QWeb 报表、网站页、邮件模板、部分 前端动态片段(与 OWL 并存,趋势是前端逐步 OWL 化)。模板在服务端由 Python QWeb 引擎 渲染,或在浏览器由 JavaScript QWeb 渲染(能力集略有不同)。
13.1 模板指令
13.1.1 知识要点
| 指令 | 作用 |
|---|---|
t-if / t-elif / t-else |
条件渲染 |
t-foreach + t-as |
循环;t-key(若版本支持)稳定列表项 |
t-set |
定义变量或整块内容 |
t-esc |
转义为文本,防 HTML 注入 |
t-out |
新版本推荐的安全输出(行为以文档为准,可结合 markup) |
t-raw |
不转义,仅用于可信 HTML |
t-call |
调用子模板 |
t-att-* / t-attf-* |
动态属性与格式化属性 |
13.1.2 案例
案例 A:表格循环
<table class="table table-sm">
<thead><tr><th>书名</th><th>数量</th></tr></thead>
<tbody>
<t t-foreach="doc.line_ids" t-as="line">
<tr>
<td t-esc="line.book_id.name"/>
<td t-esc="line.qty"/>
</tr>
</t>
</tbody>
</table>
案例 B:条件块
<t t-if="doc.partner_id">
<p>借阅人:<span t-esc="doc.partner_id.name"/></p>
</t>
<t t-else="">
<p>未指定借阅人</p>
</t>
案例 C:动态 class
<div t-attf-class="badge {{ 'bg-success' if doc.state == 'done' else 'bg-secondary' }}">
<span t-esc="doc.state"/>
</div>
13.1.3 截图占位

13.1.4 本节练习
- 实操:为借阅单报表增加 空列表 时显示「无明细」提示(
t-if)。 - 简答:
t-foreach中line_index、line_first等内置变量(若可用)的用途? - 判断:
t-esc与t-out在任何版本行为完全一致。( )
参考答案提示:3. 错,以版本文档为准。
13.2 数据输出与变量(新增)
13.2.1 知识要点
- 用户生成内容(昵称、评论、富文本)禁止直接
t-raw。 t-out:在支持的环境中可配合 markup 等安全策略(查阅当前版本)。t-set:t-value为表达式;- 无
t-value时可在标签内放 整块子模板 赋给变量。
- 作用域:内层
t-set可 遮蔽 外层;t-call内联变量规则见 13.3。
13.2.2 案例
<t t-set="title" t-value="doc.name"/>
<h1 t-out="title"/>
<t t-set="warning">
<p class="text-danger">超期未还</p>
</t>
<t t-out="warning"/>
第二段若 warning 含不可信 HTML,仍须消毒或改 t-esc 纯文本。
13.2.3 截图占位

13.2.4 本节练习
- 改错:用户昵称
user_input使用t-raw="user_input"→ 如何改? - 简答:邮件客户端打开 HTML 邮件时,与浏览器 XSS 有何相似风险?
参考答案提示:1. t-esc 或 t-out + 可信消毒链路。2. 可执行脚本/钓鱼链接风险。
13.3 子模板调用(新增)
13.3.1 知识要点
t-call="module.template_id":执行子模板,默认传入当前上下文(doc、company等)。- 传参:在
t-call体内使用t-set,对子模板相当于 局部变量。 - 复用:页眉/页脚/条款段落多处
t-call,减少复制粘贴。
13.3.2 案例
子模板 my_module.report_header
<div class="header row">
<div class="col-6" t-if="show_logo">
<img t-att-src="image_data_uri(company.logo)" style="max-height: 60px;" alt=""/>
</div>
<div class="col-6 text-end">
<strong t-esc="company.name"/>
</div>
</div>
主模板
<t t-call="my_module.report_header">
<t t-set="show_logo" t-value="True"/>
</t>
13.3.3 截图占位

13.3.4 本节练习
- 实操:抽离 页脚(页码、打印时间)为子模板,被 两个报表
t-call。 - 简答:子模板能否访问主模板未
t-set的变量?
参考答案提示:2. 可访问渲染上下文中已有对象(如 company),视 t-call 作用域规则而定,以文档为准。
13.4 Python 与 JavaScript 中的 QWeb(新增)
13.4.1 知识要点
| 场景 | 引擎 | 备注 |
|---|---|---|
| 报表 PDF/HTML | Python QWeb | ir.actions.report、_render 相关 API |
| 邮件模板 | Python QWeb | 动态字段 object |
| 网站老片段 / 部分 legacy | JS QWeb | 与 OWL 模板语法可能不同步 |
后端渲染常传入:docs/docids、doc、company、user、time、format_* 等辅助对象(以报表 render 上下文为准)。
13.4.2 案例(概念)
# 伪代码:在模型或报表逻辑中触发渲染,具体 API 见 ir.actions.report
# report = self.env['ir.actions.report']._render_qweb_pdf(report_ref, res_ids=doc.ids)
13.4.3 截图占位

13.4.4 本节练习
- 简答:为何邮件模板也要防止 XSS?
- 拓展:在源码中搜索
render_template,记录一种调用路径(模块名即可)。
13.5 报表排版常用技巧
13.5.1 知识要点
- 使用
web.external_layout/web.basic_layout统一页眉页脚(以版本模板为准)。 - CSS:打印媒体
@media print;避免依赖复杂 JS。 - 分页:大表格
thead重复、避免单行切断(CSSpage-break-inside)。
13.5.2 案例
<t t-call="web.external_layout">
<div class="page">
<h2>借阅收据</h2>
...
</div>
</t>
13.5.3 截图占位

13.5.4 本节练习
- 实操:调大某表格字号后检查 分页是否截断行。
- 简答:QWeb 报表中嵌入 外链图片 的利弊?
本章综合练习
- 实现:借阅收据 PDF:公司抬头、借阅人、明细表、页脚页码(
t-call复用页脚)。 - 对比:同一批借阅数据,用 列表视图导出 与 QWeb 报表 各适合什么业务角色?
- 安全审计:标出你项目某邮件模板中所有
t-raw,分类为「可信 / 需整改」。 - 阅读:官方 QWeb 文档中关于 deprecated 指令(若有)一条。
本章对应白皮书目录:第十三章 QWeb 模板。