Odoo入门(十)—— Qweb之报告视图

创建一个QWeb 报告视图

  • 生成报告对于业务应用是十分有价值的一个功能。从Odoo8.0开始就是使用内置QWeb报告引擎来生成报告的.报告引擎通过QWeb模板来把html文档转换为pdf格式。
  • 在本章中。我们来为我们的To Do应用添加一个生产业务报告的功能。

安装wkhtmltopdf包(不是python包)

  • 为了正确的生成报告,我们需要安装 wkhtmltopdf 这个库.从它的名字中就可以看到 Webkit HTML to PDF . Odoo使用它来把一个html页面渲染成PDF文档。
  • 这里需要注意的是 wkhtmltopdf 在使用过程中,如果有不打印页头跟页脚的情况。可能是由于这个库的版本问题引起的(教材中是推荐使用0.12.1版本)。在实际操作使用中,我使用的环境是macos,Odoo10版本,安装的版本是0.12.1。不同操作系统需要根据自身情况下载不同的包。下载地址在为 下载链接. 由于我使用的是macos,直接下载软件包进行安装即可(注意,删除时mac使用sudo uninstall-wkhtmltox)。
  • Ubuntu的用户可以直接下载deb软件包进行安装。最后可以使用命令wkhtmltopdf --version来查看是否安装成功.
  • 安装好wkhtmltopdf后, 我们在Odoo客户端执行点击print调用转化为pdf方法就不会显示You need Wlhtmltopdf to print a pdf version of the report’s这条错误信息了.

创建业务报告

  • 为了学习,我们创建一个新的模块来单独讲解建立报告功能。我们的报告最终的效果像下面这样 报告视图

  • 我们为这个新模块起名为todo_report.创建模块还是跟以往一样的步骤。创建__init__.py.添加下面代码到__manifest__.py中.

    {
        'name': 'To-Do Report',
        'description': 'Report  for todo-task',
        'author': 'xer',
        'depends': ['todo_kanban', 'website'],
        'data': ['views/todo_report.xml',
    	     'reports/todo_model_report.xml'
    	     ]
    }
    

创建reports子文件夹,新建todo_report.xml文件.填入下面的xml代码

<odoo>
  <report id="action_todo_task_report"
      string="To-do Tasks"
      model="todo.task"
      report_type="qweb-pdf"
      name="todo_report.report_todo_task_template"/>
</odoo>
- `<report>`标签是`ir.actions.report.xml`模型的缩写形式.它是一个独特的客户端动作,它的数据能够通过开发者模式 **Settings | Technical | Reports** 菜单获取到。
- 我们安装完todo_report模块后,会在to-do task表单视图上看到 **Print** 按钮(在 **More** 按钮的左边). Print 按钮中包含了我们刚定义的report动作.
这个动作目前无法起效,因为我们还没有定义这个报告的内容.我们需要在`todo_report.xml`文件中添加QWeb模板.需要注意的是`name`属性标识了我们使用的模板,这`name`属性中,模块前缀必须要添加.

## QWeb 报告模板

- 报告通常遵循的基本骨架如下:

```xml
	<template id="report_todo_task_template">
	  <t t-call="report.html_container">
	    <t t-call="report.external_layout">
		<div class="page">
		    <!-- Report page content -->
		  </div>
		 </t>
	     </t>
	</template>
  • 上面代码中,最为重要的是t-call这个指令, 使用了标准的report架构.report.html_container模板进行了最为基础的设置来支持HTML文档。report.external_layout模板处理了报告的头部跟页脚,使用我们相应用户设置中的公司信息。这里还可以使用report.internal_layout ,它使用最为基础的头部布局.
  • 现在我们已经构造了最为基础的report模块架构.可以看到,由于report 使用的是QWeb模板,我们可以用Odoo的继承制对它进行继承扩展.

在报告中呈现数据

  • 与看板视图不同,报告视图是在服务端被渲染的,所以这里就需要使用Python语言来编写QWeb模板。

  • 服务端的QWeb编写,我们可以获得的变量

    • docs : 包含记录的可迭代集合
    • doc_ids : 记录的IDs
    • doc_model : 标识记录所用的模型。例如 todo.task.
    • time : 与Python的time库管理
    • user : 当前执行报告功能的用户
    • res_company : 当前用户的公司
  • 报告内容是使用HTML语言编写的,字段的值可以通过t-field来获取,我们还可以使用t-field-options属性来指定特殊的窗口组件来渲染字段内容. 下面来设计我们报告的显示页面内容:

    <!--report page content-->
        <div class="row bg-primary">
         <div class="col-xs-3">
          <span class="glyphicon glyphicon-pushpin"/>
    	     What
         </div>
           <div class="col-xs-2">Who</div>
           <div class="col-xs-1">When</div>
           <div class="col-xs-3">Where</div>
           <div class="col-xs-3">Watchers</div>
         </div>
    	<t t-foreach="docs" t-as="o">
    	<div class="row">
    	      <!-- Date Row Content -->
    	  </div>
    	 </t>
    
  • 内容的布局可以参考Twittrt Bootstrap HTML 网格系统。在一个容器中,Bootstrap有12列网格布局。添加新的一行可以使用<div class="row">来实现、在一行中,我们有12个单元格。每个单元格可以使用<div class='col-xs-N">来定位,N代表列的跨度。上面的代码其实就是展示了第一行的默认数据。如图 定义第一行数据

  • 因为report视图是在后端渲染的,数据记录可以看做是对象,所以我们可以使用点操作符来获取关联模型中的字段。

<div class="col-xs-3">
	<h4><span t-field="o.name"/></h4>
</div>
<div class="col-xs-2">
	<span t-field="o.user_id"/>
</div>
<div class="col-xs-1">
	<span t-field="o.date_deadline"/>
	<span t-field="o.amount_cost" t-field-options='{ "widget":"monetary", 
													"display_currency": "o.currency_id"}'/>
</div>
<div class="col-xs-3">
	<div t-field="res_company.partner_id" t-field-options="{ 'widget': 'contact',
														    'fields':['address','name','phone','fax'], 
														    'no_marker': true}"/>
</div>
	<div class="col-xs-3">
	<!--Render followers-->
</div>
  • 就像我们在代码中看到的,字段能够使用额外的可选参数通过t-field-options.这与表单视图中我们使用options属性十分类似.我们添加的额外属性中widget是用来改变字段渲染时所选择的显示窗口化部件的.
  • 举例来说,我们使用了contract小部件,用来格式化地址的显示. 默认的contract窗口部件会显示一些小图片(像是电话图片),但是我们使用了no_market="true" 来禁用这些小图表.

渲染图片

  • 在报告的最后一列中,我们会添加一个展示问题关注者头像的列表。我们会使用Bootstrap 中的media-list部分,通过循环遍历每个关注者来渲染它们的头像列表视图。

     <!--Render followers-->
          <ul class="media-list">
    	  <t t-foreach="o.message_follower_ids" t-as="f">
    	    <li t-if="f.partner_id.image_small" class="media-left">
    	     ![]('data:image/png;base64,%s' %f.partner_id.image_small)
    	     <span class="media-body" t-field="f.partner_id.name"/>
    	    </li>
    	 </t>
          </ul>
    

二进制类字段可以通过 base64 形式表现。因此我们直接使用t-att-src属性动态的展示图片src地址可以了.

统计总数跟计算总数

  • 报告中的一个常用功能就是提供数据统计总和。我们可以使用Python表达式来计算这些总的统计数。

  • 在循环遍历的<t t-foreach>标签结束后,我们使用最后一行来进行数据的汇总统计:

     <div class="row">
       <div class="col-xs-3">
           Count: <t t-esc="len(docs)"/>
       </div>
     <div class="col-xs-2"/>
     <div class="col-xs-1">
          Total:
        <t t-esc="sum([o.amount_cost for o in docs])" />
     </div>
      <div class="col-xs-3"/>
      <div class="col-xs-3"/>
    </div>
    
  • len()这个python语句是用来对集合的总长度进行计算的方法。我们还可以使用sum()方法来对列表中的数据进行求和.

  • 我们还可以实现实时统计功能.这就需要使用t-set来首先确定一个初始值.下面我们添加统计点开任务记录后对每个任务的关注者人数的统计功能: 首先在遍历开始前设置默认值:

<t t-set="follower_count" t-value="0" />

然后在循环中添加实时统计代码

<!--Running total-->
    <t t-set="follower_count" t-value="follower_count + len(o.message_follower_ids)"/>
        Accumulated #<t t-esc="follower_count"/>

定义打印用纸格式

  • 在html页面上我们的报告看起来已经十分完整了,但是在实际的打印过程中,可能会有格式上的错误。下面我们需要对转为PDF后纸张的格式规范。

        <!--the print paper setting-->
        <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>
    
  • 上面的纸张是以欧洲的A4标准规范的。A4纸的具体定义在addons/report/data/report_paperformat.xml这个xml文件中.我们可以通过 Settings | Technical | Reports | Paper Format 选项中查看默认使用的纸张格式.

  • 我们已经制定好纸张格式了,要在报告视图中使用这个格式需要使用paperformat属性来进行关联.

    <!--use the setting paper with attr 'paperformat'-->
    <report id="action_todo_task_report"
            string="To-do Tasks"
            model="todo.task"
            report_type="qweb-html"
            name="todo_report.report_todo_task_template"
            paperformat="paperformat_euro_landscape"
     />

在报告视图中使用语言翻译

  • 为了能在报告中使用翻译功能,我们需要在模板中使用 <t t-call>元素,使用它的t-lang属性来指定语言.
  • t-lang属性的值使用语言代码,类似于es,en_US这样的形式。这就需要通过字段来进行值的传递。举个例子,通常在Partner模型中把合作伙伴使用的语言存贮在lang字段中。使用时直接通过partner_id.lang取出语言代码即可。在我们的例子中,我们并没有合作伙伴,那就使用任务负责人的语言设置。user_id.lang.
    <template id="report_todo_task_translated">
        <t t-call="todo_report.report_todo_task_template"
           t-lang="user.lang">
            <t t-set="docs"
               t-value="docs"/>
        </t>
    </template>

直接使用我们的SQL语句来构造报告

  • 我们刚建立的报告是基于记录集的。但是在有些案例中,我们需要直接从底层数据库中获取数据来生成报告。 我们来创建reports/todo_task_report.py:

    from odoo import models, fields
    
    class TodoReport(models.Model):
        _name = 'todo.task.report'
        _description = 'To-do Report'
        _sql = """
    	    CREATE OR REPLACE VIEW todo_task_report AS
    	    select *
    	    from todo_task
    	    where active = TRUE 
    	    """
        name = fields.Char('Description')
        is_done = fields.Boolean('Done?')
        active = fields.Boolean('Active?')
        user_id = fields.Many2one('res.users', 'Responsible')
        date_deadline = fields.Date('Deadline')
    
  • sql属性会直接重写Odoo从底层获取数据的sql语句。 基于我们构造的TodoReport模型,我们可以使用xml文件来对这个模型进行报告视图的生成。构造reports/todo_model_report.xml:

<odoo>
    <report id="action_todo_model_report"
            string="To-do Special Report"
            model="todo.task"
            report_type="qweb-html"
            name="todo_report.report_todo_task_special"
            />
    <template id="report_todo_task_special">
        <t t-call="report.html_container">
            <t t-call="report.external_layout">
                <div class="page">
                    <table class="table table-striped">
                        <tr>
                            <th>Title</th>
                            <th>Owner</th>
                            <th>Deadline</th>
                        </tr>
                        <t t-foreach="docs" t-as="o">
                            <tr>
                                <td class="col-xs-6">
                                    <span t-filed="o.name"/>
                                </td>
                                <td class="col-xs-3">
                                    <span t-filed="o.user_id"/>
                                </td>
                                <td class="col-xs-3">
                                    <span t-filed="o.date_deadline"/>
                                </td>
                            </tr>
                        </t>
                    </table>
                </div>
            </t>
        </t>
    </template>
</odoo>