报表是业务应用非常有价值的功能,内置的 QWeb 引擎是报表的默认引擎。使用 QWeb 模板设计的报表可生成 HTML 文件并被转化成 PDF。也就是说我们可以很便捷地利用已学习的 QWeb 知识,应用到业务报表中。本文中我们将为图书馆应用添加一个报表,复习 QWeb生成报表的关键技巧。包括像汇总一类计算、翻译和纸张样式打印。
本文主要内容有:
- 安装wkhtmltopdf
- 创建业务报表
- QWeb 报表模板
- 在报表中展示数据
- 渲染图片
- 报表汇总
- 定义纸张格式
- 在报表中启用语言翻译
- 使用自定义 SQL 建立报表
开发准备
我们将继续使用library_app插件模块进行学习,该模块在第三章 Odoo 13 开发之创建第一个 Odoo 应用中初次创建,然后在第五章 Odoo 13开发之导入、导出以及模块数据和第六章 Odoo 13开发之模型 – 结构化应用数据中进行了改进。相关代码请参见 GitHub 仓库。本文完成后代码也请参见GitHub 仓库。
安装wkhtmltopdf
要正确地生成报表,应安装wkhtmltopdf工具的推荐版本,该工具的名称表示Webkit HTML to PDF。Odoo使用它来将渲染的 HTML 页面转化为 PDF 文档。有些版本的wkhtmltopdf库已知存在问题,比如不打印页面头部和底部,所以需挑选使用的版本。从Odoo 10开始,官方支持了0.12.5版本,这也是官方推荐的版本。
小贴士:官方Odoo项目有一个 wiki 页面,保持了对于wkthtmltopdf使用的信息和推荐。可通过 GitHub 进行查看。
不幸的是你的主机系统,不论是Debian/Ubuntu或其它系统,所提供的安装包版本都不太一致。所以我们应下载和安装对于当前操作系统和 CPU 类型的推荐版本包。下载链接请见 GitHub。
首先应确保系统中所安装的不是错误的版本:
wkhtmltopdf --version
如果上述命令打印的结果不是我们需要的版本,应对其进行卸载。在Debian/Ubuntu系统中,使用的命令如下:
sudo apt-get remove --purge wkhtmltopdf
下一步我们需要下载适合我们系统的安装包并进行安装。通过GitHub下载链接进行查看。对于0.12.5,最新 Ubuntu 安装版本是针对Ubuntu 14.04 LTS稳定版,但对其后的Ubuntu系统应该同样生效。我们在最近发布的Ubuntu 64系统中进行安装,下载命令如下:
wget "https://github.com/wkhtmltopdf/wkhtmltopdf/releases/download/0.12.5/wkhtmltox_0.12.5-1.bionic_amd64.deb" -O /tmp/wkhtml.deb
下一步应进行安装。安装本地deb文件并不会自动安装依赖,因此需要执行第二步来完成安装:
sudo dpkg -i /tmp/wkhtml.deb
这时可能会显示缺少依赖的错误,以下命令可解决这一问题:
sudo apt-get -f install
现在,我们可以检查wkhtmltopdf库是否正确安装并确认是否为所需版本:
$ wkhtmltopdf --version
wkhtmltopdf 0.12.5 (with patched qt)
此时Odoo服务的启动日志就不会再提示You need Wkhtmltopdf to print a pdf version of the report的信息了。
创建业务报表
我们会继续使用前面文章所使用的library_app模块,进添加实现报表的文件。我们将创建的报表会长成这样
报表文件应放在模块子文件夹/reports中。首先我们来添加一个reports/library_book_report.xml数据文件,不要忘记在__manifest__.py
文件的 data 下导入该文件。先在reports/library_book_report.xml文件中声明一个新报表:
<?xml version="1.0"?>
<odoo>
<report id="action_library_book_report"
string="Library Books"
model="library.book"
report_type="qweb-pdf"
name="library_app.report_library_book_template" />
</odoo>
<report>
标签是对向ir.actions.report写入数据的简写形式,这个模型是客户操作的特殊类型。它的数据可通过Settings > Technical > Actions >Reports菜单进行查看。
注意:Odoo13开始,report的模型从 ir.actions.report.xml修改为ir.actions.report
小贴士:在设计报表时,我们可能更倾向保留为report_type=”qweb-html”然后在完成时再修改为qweb-pdf文件。这样在QWeb模板中可更快速的生成报表并且更易于检查 HTML 结果。
执行完模块升级(~/odoo-dev/odoo/-bin -d dev13 -u library_app)后,图书表单视图中会在顶部显示一个 Print 按钮(列表视图中也有),它在Actions按钮的左侧,其中包含添加的运行报表的选项(Library Books)。
现在还无法生成报表,因为我们还没有进行定义。这是一个QWeb报表,因此需要用到QWeb模板。name 属性标识所使用的模板。与其它标识符引用不同,name 属性中需要添加模块前缀,我们必须使用完整的引用名称<module_name>.<identifier_name>
。
QWeb 报表模板
在下面的代码可以看出,这个报表遵循一个基本框架。仅需在reports/library_book_report.xml文件元素后添加如下代码:
<template id="report_library_book_template">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<div class="page">
<!-- Report header content -->
<t t-foreach="docs" t-as="o">
<!-- Report row content -->
</t>
<!-- Report footer content -->
</div>
</t>
</t>
</template>
这里最重要的元素是使用标准报表结构的t-call指令。web.html_container模板进行支持 HTML 文档的基本设置。web.external_layout模板使用相应公司的相关设置处理报表头部和底部。可将其替换为web.internal_layout模板,它将只使用一个基本的头部。
ℹ️ Odoo 11中的修改 对报表的支持布局从report 模块移到了 web 模块中。也就是说此前版本中使用report.external_layout或report.internal_layout的引用 ,在11.0中引用应修改为web.<…>。
external_layout模板可由用户自定义,Odoo 11引入了这一选项,在Settings > General Settings菜单中,然后相关内容在Business Documents > Document Template版块:
这里我们可以点击Change Document Template来从几个可用的模板中选取,甚至是点击Edit Layout来自定义所选模板的 XML。这一个报表框架适用于列表式报表,即报表中每条记录显示为一行。报表头部通常显示标题,底部区域则显示汇总。
另一种格式是文档报表,每条记录是单独一页,比如邮件。这种情况报表结构如下:
<template id="report_todo_task_template">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<t t-foreach="docs" t-as="o">
<div class="page">
<!-- Report content -->
</div>
</t>
</t>
</t>
</template>
我们会创建一个列表式报表,所以还会使用此前的框架。现在我们已经有了基本框架。既然报表是QWeb模板,那么它也可以像其它视图那样进行继承。报表中使用的QWeb模板可使用常规视图继承使用的 XPath 表达式来进行继承。
补充:此时点击打印会输出一个空白的 PDF 文件。
在报表中展示数据
与看板视图不同,报表中的QWeb模板在服务端进行渲染,因此使用Python QWeb来实现。我们可以将其看作相同规格的两种实现,需要注意其中的一些区别。
首先这里的QWeb表达式由 Python 语法运行,而非JavaScript。对于最简的表达式几乎没有区别,但更为复杂的运算则可能存在差别。表达式运行上下文也不同,对于报表可使用如下变量:
- docs是要打印记录的可迭代集合
- doc_ids是一个要打印记录的 ID 列表
- doc_model指定记录的模型,如library.book
- time是对Python时间库的引用
- user是运行报表的用户记录
- res_company是当前用户的公司记录
可使用t-field来引用字段值,并可使用t-options来进行补充指定渲染字段内容的具体组件。
ℹ️Odoo 11中的修改 在此前的 Odoo 版本中,使用的是t-field-options属性,但在 Odoo 11中淘汰了该属性,改用t-options属性。
例如,假设doc表示一条具体记录,代码如下:
<t t-field="doc.date_published"
t-options="{'widget': 'date'}" />
现在我们可以开始设计报表的页面内容了。
小贴士:不幸的是官方文档中并没有涉及QWeb支持的组件及其选项。所以当前对其做进一步的了解只能是通过阅读相应源码。可访问ir_qweb_fields.py,查找继承ir.qweb.field的类,get_available_options() 方法可有助了解支持的选项。
报表内容由HTML书写,并且使用了Twitter Bootstrap 4来帮助设计报表布局。在网页开发中大量使用了Bootstrap,有关Bootstrap的完整指南请见官方网站。
以下为渲染报表头部的 XML 代码,应放在<div class=”page”>
中并替换掉现有的<t t-foreach=…>
元素:
<!-- Report header content -->
<div class="container">
<div class="row bg-primary">
<div class="col-3">Title</div>
<div class="col-2">Publisher</div>
<div class="col-2">Date</div>
<div class="col-3">Publisher Address</div>
<div class="col-2">Authors</div>
</div>
<t t-foreach="docs" t-as="o">
<div class="row">
<!-- Report row content -->
</div>
</t>
<!-- Report footer content -->
</div>
内容的布局使用了Twitter Bootstrap的HTML网格系统。总的来说Bootstrap使用13列的网格布局,此处网格在<div class=”container”>元素中。
ℹ️Odoo 13中的修改 现在Odoo使用Bootstrap 4,它对此前 Odoo 版本中使用的Bootstrap 3并没有保持向后兼容。对于从Bootstrap 3改为Bootstrap 4的小技巧,可参照 Odoo 中关于这一话题的 Wiki 页面。
可使用<div class=”row”>
来添加行。每行中还有多个单元格,分别占用不同列数,总计应为13列。每个单元格可通过<div class=”col-N”>
来进行定义,其中 N 表示占用列的数量。
小贴士:Bootstrap 4 在其大部分构件中使用了 CSS 弹性盒子布局,已知wkhtmltopdf 对弹性盒子的功能并不都能很好的支持。因此如果有些地方效果不对,请尝试使用其它元素或方法,如 HTML 表格。
此处我们为头部行添加了标题,然后t-foreach循环遍历每条记录并在各行中进行渲染。因为渲染由服务端完成,记录都是对象,我们可使用点号标记来从关联数据记录中访问字段。这也让关联字段的数据访问变得更为容易。注意这在客户端渲染的QWeb视图中是无法使用的,比如网页客户端的看板视图。
以下是在<div class=”row”>
元素中的记录行内容XML:
<!-- Report row content -->
<div class="col-3">
<h4><span t-field="o.name" /></h4>
</div>
<div class="col-2">
<span t-field="o.publisher_id" />
</div>
<div class="col-2">
<span t-field="o.date_published"
t-options="{'widget': 'date'}"/>
</div>
<div class="col-3">
<span t-field="o.publisher_id"
t-options='{
"widget": "contact",
"fields": ["address", "email", "phone", "website"],
"no_marker": true}'/>
</div>
<div class="col-2">
<!-- Render authors -->
</div>
可以看到字段可通过t-options属性添加额外的选项,内容为包含带有widget键的 JSON 字典。更为复杂的示例是contact组件,用于格式化地址。我们使用它来渲染出版商地址o.publisher_id。默认contact 组件显示地址时带有图像,类似电话图标。no_marker=”true”选项禁用了这一显示。
补充:no_marker=”true”禁用的地址图标如上所示
渲染图片
我们报表最后一列为一组带有头像的作者。我们将通过遍历来渲染出每个作者,并使用Bootstrap媒体对象:
<!-- Render authors -->
<ul class="list-unstyled">
<t t-foreach="o.author_ids" t-as="author">
<li class="media">
<span t-field="author.image_256"
t-options="{'widget': 'image'}" />
<div class="media-body">
<p class="mt-0">
<span t-field="author.name" />
</p>
</div>
</li>
</t>
</ul>
此处我们遍历了author_ids,使用字段图像组件<t t-field=”…” t-options=”{‘widget’: ‘image’}”>
对每个作者的头像进行了渲染,然后还有姓名。
报表汇总 报表中经常需要提供汇总。这可借由 Python 表达式来计算总额。在闭合标签之后,我们添加最后一行用于汇总:
<!-- Report footer content -->
<div class="row">
<div class="col-3">
Count: <t t-esc="len(docs)" />
</div>
<div class="col-2" />
<div class="col-2" />
<div class="col-3" />
<div class="col-2" />
</div>
len() Python函数用于计算集合元素的数量。类似地,汇总也可以使用sum()来对一组值进行求和运算。例如,可使用如下列表推导式来进行总额运算:
<t t-esc="sum([x.price for x in docs])" />
可以把这个列表推导式看作一个内嵌的循环。有时我们需要贯穿报表执行一些计算,如流动汇总(running total),汇总至当前记录。这可通过t-set 来定义累加变量在每一行进行更新来实现。为描述这一功能,我们来计算作者数的累加。首先在docs 记录集 t-foreach 循环前初始化变量:
<!-- Running total: initialize variable -->
<t t-set="author_count" t-value="0" />
然后在循环内,将记录的作者数添加到变量中。我们这里显示在书名之后,并在每行打印出当前总数:
<!-- Running total: increment and present -->
<t t-set="author_count" t-value="author_count + len(o.author_ids)" />
(Accum. authors: <t t-esc="author_count" />)
定义纸张样式
到这里我们的报表的 HTML 显示没有问题了,但在打印的 PDF 页面中还不够美观。使用横向页面显示结果会更好,因此下面就来添加纸张样式。在报表 XML 文件的最上方添加如下代码:
<record id="paperformat_euro_landscape"
model="report.paperformat">
<field name="name">European A4 Landscape</field>
<field name="default" eval="True" />
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Landscape</field>
<field name="margin_top">40</field>
<field name="margin_bottom">23</field>
<field name="margin_left">7</field>
<field name="margin_right">7</field>
<field name="header_line" eval="False" />
<field name="header_spacing">35</field>
<field name="dpi">90</field>
</record>
这是对European A4格式的一个拷贝,这在data/report_paperformat_data.xml文件中定义的base 模块中,但这里将排列方向由纵向改为了横向。定义的纸张样式可通过后台Settings > Technical > Reporting > Paper Format菜单进行查看。
现在就可在报表中使用它了。默认的纸张样式在公司设置中定义,但我们也可以为特定报表指定纸张样式。这通过在报表操作中的paperfomat属性来实现。下面来编辑打开报表使用的操作,添加这一属性:
<report id="action_library_book_report"
...
paperformat="paperformat_euro_landscape" />
在报表中启用语言翻译
要在报表中启用翻译,需要使用带有t-lang属性的元素在模板中调用翻译方法。t-lang需传入一个语言代码来运行,如es或en_US。它需要可以找到所需使用语言的字段名。一种方式是使用当前用户的语言,为此,我们定义一个外层翻译报表来调用要翻译的报表,使用t-lang属性来设置语言来源:
<report id="action_library_book_report"
...
name="library_app.report_library_book_translated"
paperformat="paperformat_euro_landscape" />
<template id="report_library_book_template">
<t t-call="library_app.report_library_book_translated"
t-lang="user.lang" />
</template>
本例中,每本书都使用了用户的语言user_id.lang来进行渲染。
有些情况下,我们可能需要每条记录以指定语言进行渲染。比如在销售订单中,我们可能要各条记录按照对应合作方/客户的首选语言进行打印。假设我们需要每本书按照对应出版商的语言进行渲染,QWeb模板可以这么写:
<template id="report_library_book_translated">
<t t-foreach="docs" t-as="o">
<t t-call="library_app.report_library_book_template"
t-lang="o.publisher_id.lang">
<t t-set="docs" t-value="o" />
</t>
</t>
</template>
以上我们对记录进行了迭代,然后每条记录根据记录上的数据使用相应的语言进行报表模板的调用,本例为出版商的语言publisher_id.lang。
补充:以上代码运行时每条记录都会带有一个头部,如需按列表显示还需将头部抽象到循环之外
使用自定义 SQL 创建报表
前面我们所创建的报表都是基于常规记录集,但在有些情况下我们需要执行一些在QWeb模板中不易于处理的数据转换或累加。一种解决方法是写原生 SQL 查询来创建我们所需的数据集,将结果通过特殊的模型进行暴露,然后基于这一数据集来生成报表。
我们创建reports/library_book_report.py文件来讲解这一情况,代码如下:
from odoo import models, fields
class BookReport(models.Model):
_name = 'library.book.report'
_description = 'Book Report'
_auto = False
name = fields.Char('Title')
publisher_id = fields.Many2one('res.partner')
date_published = fields.Date()
def init(self):
self.env.cr.execute("""
CREATE OR REPLACE VIEW library_book_report AS
(SELECT *
FROM library_book
WHERE active=True)
""")
要加载以上文件,需要在模块的顶级__init__.py
文件中添加from . import reports,并在reports/__init__.py
文件中添加from . import library_book_report。
_auto属性用于阻止数据表的自动创建。我们在模型的init()方法中添加了替代的 SQL。它会创建数据库视图,并提供报表所需数据。以上 SQL 查询非常简单,旨在说明我们为视图可以使用任意有效的 SQL查询,如对额外数据执行累加或计算。
我们还需要声明模型字段来让 Odoo 知道如何正确处理每一条记录中的数据。同时不要忘记为新模型添加安全访问规则,否则将无法使用该模型。下面在security/ir.model.access.csv文件中添加下行:
access_library_book_report,access_library_book_report,model_library_book_report,
library_group_user,1,0,0,0
还应注意这是一个全新的不同模型,与图书模型的访问规则并不相同。下一步基于该模型我们可以使用reports/library_book_sql_report.xml新增一个报表:
<?xml version="1.0"?>
<odoo>
<report id="action_library_book_sql_report"
string="Library Book SQL Report"
model="library.book.report"
report_type="qweb-html"
name="library_app.report_library_book_sql" />
<template id="report_library_book_sql">
<t t-call="web.html_container">
<t t-call="web.external_layout">
<div class="page">
<!-- Report page content -->
<table class="table table-striped">
<tr>
<th>Title</th>
<th>Published</th>
<th>Date</th>
</tr>
<t t-foreach="docs" t-as="o">
<tr>
<td class="col-xs-6">
<span t-field="o.name" />
</td>
<td class="col-xs-3">
<span t-field="o.publisher_id" />
</td>
<td class="col-xs-3">
<span t-field="o.date_published"
t-options="{'widget': 'date'}" />
</td>
</tr>
</t>
</table>
</div>
</t>
</t>
</template>
</odoo>
对于更为复杂的情况,我们还会需要用户输入参数,这时可以使用不同的方案:向导。为此我们应创建一个临时模型来存储用户的报表参数。因为这是由代码生成的,所以我们可以使用所需的任意逻辑。
强烈推荐学习已有的相似报表来获取灵感。一个不错的例子是Leaves菜单选项下的Leaves by Department,相应的临时模型定义可以参见addons/hr_holidays/wizard/hr_holidays_summary_employees.py。
首先在views/library_menu.xml文件中添加如下内容:
<act_window id="action_library_book_report_window"
name="Book Report"
res_model="library.book.report"
view_mode="tree,form"
/>
<menuitem id="menu_library_book_report"
name="Book Report"
parent="menu_library"
action="action_library_book_report_window"
/>
在__manifest__.py
文件中引入前述的 XML 文件后更新模块
总结
前面一篇文章中,我们学习了QWeb以及如何使用它来设计看板视图。本文中我们学习了QWeb报表引擎,以及使用QWeb模板语言创建报表最为重要的一些技术。
下一篇文章中,我们将继续使用QWeb,这次是创建网页。我们将学习书写网页控制器,为我们的网页提供更丰富的功能。
☞☞☞第十三章 Odoo 13开发之创建网站前端功能