第十九章 报表

我们这里所说的报表,实际上指的是报表动作(ir.actions.report),不是应用意义上的报表文件. 报表动作是连接WC与QWeb引擎的桥梁, 它负责将模型数据与QWeb的模板文件相结合, 最终渲染成为我们看到的报表文件.我们编程意义上的定义一个报表,实际上也是定义一个报表动作,本章我们就来详细了解以下odoo中报表的工作机制.

报表的工作原理

我们在第一部分介绍报表时只简单介绍了如何定义一个报表,并没有详细介绍报表的工作机制. 实际上,我们在第一部分定义的报表就是一个报表动作.

<record model="ir.actions.report" id="sale_tag_report.report">
    <field name="name">标签打印</field>
    <field name="model">sale.order</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">sale_tag_report.tag</field>
    <field name="print_report_name">(object.name)</field>
    <field name="binding_model_id" ref="sale.model_sale_order"/>
</record>

我们在定义了报表动作之后, 当我们在Web Client中点击了打印报表按钮之后, Web Client会向Controller发起一个路径为/web/action/load的请求, 该请求携带了要请求的动作ID和相关的模型ID和记录.

Controller在收到请求以后,会校验参数中的动作ID, 如果确认为报表动作那么会在上下文中加入bin_size参数并设置为True, 然后继续读取动作的值,并返回给前端.

下面是一个报表动作的返回示例:

{
  "id": 297,
  "name": "Invoices",
  "type": "ir.actions.report",
  "binding_type": "report",
  "report_type": "qweb-html",
  "report_name": "account.report_invoice_with_payments",
  "xml_id": "account.account_invoices",
  "help": false,
  "binding_model_id": [
    433,
    "Journal Entry"
  ],
  "binding_view_types": "list,form",
  "display_name": "Invoices"
}

WebClient在收到动作以后,根据不同地报表类型进行不同的处理工作(关于WebClient相关的知识请参考第四部分),发起不同类型的请求.

整个工作的流程图如下:

报表动作

在odoo中报表动作有三种类型,分别是:

  • html(qweb-html)
  • pdf (qweb-pdf)
  • text (qweb-text)

html类型是HTML页面的报表,如果PDF类型报表不可用(缺少Htmltopdf插件)时,会自动使用html类型的报表.而text类型的报表是用来直接驱动打印机进行打印的命令式报表.

class IrActionsReport(models.Model):
    _name = 'ir.actions.report'
    _description = 'Report Action'
    _inherit = 'ir.actions.actions'
    _table = 'ir_act_report_xml'
    _sequence = 'ir_actions_id_seq'
    _order = 'name'

我们从报表动作的定义可以看出, 报表动作本质上还是动作的一种, 只不过由于相对比较特殊被单独拎了出来. 报表存储在数据库中的ir_act_report_xml表中.

每个报表都会绑定一个数据模型,动作报表中的model字段用来存储数据模型的名称,model_id则是根据model名称对应的Many2one类型的字段,可以被直接使用的快捷字段.

报表动作用来指定打印模板的字段是report_name, 它指定是qweb的模板文件, 在打印过程中会使用该模板进行渲染. report_file是打印模板的文件路径.

默认情况下, 报表动作不论是在表单视图或者树形列表视图中都可以打印, 用户也可以选择只在树形列表视图中勾选后进行打印, 方法就是把报表动作的Multi字段设置为True.

打印出来的文件名称也可以在print_report_name字段中自定义,该字段支持python语法动态赋值.下面是一个例子:

(object.state in ('draft', 'sent') and 'Quotation - %s' % (object.name)) or 'Order - %s' % (object.name)

object这里指的是模型的记录数据.

另外, 打印报表可以作为附件存储在系统中,当用户第二次打印的时候, 直接从附件中读取第一次打印的结果返回,不再进行重复打印. 方法是把attachment_use字段设置为True, 第一次打印的结果将被存储在attachment字段中.

打印赋值过程

我们在之前的介绍中知道了, WebClient会根据返回的动作结果再次发起请求, 最终实现报表的渲染(html)或者下载(PDF). 那么这个过程是什么样子的呢?下面我们就来详细看一下这个赋值过程.

我们知道WebClient第二次请求时,会根据不同报表类型组成URL格式:/report/, 实际上该请求对应了ReportController的路由:

@http.route([
        '/report/<converter>/<reportname>',
        '/report/<converter>/<reportname>/<docids>',
    ], type='http', auth='user', website=True)
def report_routes(self, reportname, docids=None, converter=None, **data):
    report = request.env['ir.actions.report']._get_report_from_name(reportname)
    context = dict(request.env.context)

    if docids:
        docids = [int(i) for i in docids.split(',')]
    if data.get('options'):
        data.update(json.loads(data.pop('options')))
    if data.get('context'):
        data['context'] = json.loads(data['context'])
        context.update(data['context'])
    if converter == 'html':
        html = report.with_context(context)._render_qweb_html(docids, data=data)[0]
        return request.make_response(html)
    elif converter == 'pdf':
        pdf = report.with_context(context)._render_qweb_pdf(docids, data=data)[0]
        pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
        return request.make_response(pdf, headers=pdfhttpheaders)
    elif converter == 'text':
        text = report.with_context(context)._render_qweb_text(docids, data=data)[0]
        texthttpheaders = [('Content-Type', 'text/plain'), ('Content-Length', len(text))]
        return request.make_response(text, headers=texthttpheaders)
    else:
        raise werkzeug.exceptions.HTTPException(description='Converter %s not implemented.' % converter)

ReportController的核心职责就是根据不同的报表类型分发到不同的报表渲染方法内,具体的渲染工作是由报表动作自己内部的不同方法进行处理的.

根据对_render_qweb_html,_report_qweb_pdf,_report_qweb_text方法的分析,他它们最终都指向了核心渲染方法_render_template.

def _render_template(self, template, values=None):
    """Allow to render a QWeb template python-side. This function returns the 'ir.ui.view'
    render but embellish it with some variables/methods used in reports.
    :param values: additional methods/variables used in the rendering
    :returns: html representation of the template
    :rtype: bytes
    """
    if values is None:
        values = {}

    context = dict(self.env.context, inherit_branding=False)

    # Browse the user instead of using the sudo self.env.user
    user = self.env['res.users'].browse(self.env.uid)
    website = None
    if request and hasattr(request, 'website'):
        if request.website is not None:
            website = request.website
            context = dict(context, translatable=context.get('lang') != request.env['ir.http']._get_default_lang().code)

    view_obj = self.env['ir.ui.view'].sudo().with_context(context)
    values.update(
        time=time,
        context_timestamp=lambda t: fields.Datetime.context_timestamp(self.with_context(tz=user.tz), t),
        user=user,
        res_company=user.company_id,
        website=website,
        web_base_url=self.env['ir.config_parameter'].sudo().get_param('web.base.url', default=''),
    )
    return view_obj._render_template(template, values).encode()

_render_template的作用是将一些环境变量封装起来,然后使用ir.ui.view对象的_render_template方法将报表最终渲染出来. 也就是说, 最终的渲染方法是使用的视图对象的渲染方法.

另外,在每种报表渲染之前,都会使用名叫_get_rendering_context的方法获取报表渲染前的数据变量. 如果模型自己定义了报表变量, 那么报表动作将会使用模型自己定义的变量进行渲染(_get_report_value),否则将使用统一的变量机型渲染:

  • doc_ids: 报表渲染的记录IDS
  • doc_model: 报表渲染的数据模型
  • docs: 报表渲染的数据记录集

动态报表

如果想要对报表进行动态的渲染, 只需要根据数据的值,结合t-if语法进行判断即可,这个在第一部分已经讨论过了. 这里想要阐明的是(也是客户实际需求出现过的)根据不同的记录值显示不同的报表动作名称.

举个例子, 在发货单报表打印中, 有个一报表名为Delivery Slip, 我们希望这个报表打印的名称能够根据不同的单据显示不同的名称,发货单显示为Deliver Slip, 退货单显示为Return Note.

实际上,该功能已经超出了报表定义的范畴, 涉及到了视图渲染的过程,这里简单说明优化下,实现这个功能的难点在于, 我们在渲染视图的时候, 首先调用了load_views加载视图, 而此时我们尚未读取记录数据, 也就因此无法根据记录的值改变动作的名称.

当然, 要实现这个需求也是有办法的,只不过要绕点圈子. 笔者已经解决了这个问题,并把它集成在了欧姆基础模块中.

打印报表前的校验

实际项目中,有些客户提出了想要控制打印按钮的需求, 例如, 只希望在调拨单完成时打印调拨单.

这时候,我们需要在用户点击了打印按钮之后, 系统渲染报表之前, 嵌入一个新的校验逻辑, 如果不满足客户规定的条件, 那么我们就弹出提示框给客户以警告.

笔者已经将该思路集成到了上面提到的欧姆基础模块中, 下面我们来简单看一下用法和示例:

class stock_picking(models.Model):
    _inherit="stock.picking"

    def _pre_report_action(self):
        for picking in self:
            if picking.state != 'done':
                raise UserError("Unconfirmed delivery, unable to print the delivery note.")

在需要进行打印前鉴权的模型中定义方法_pre_report_action, 然后在该方法内编写业务逻辑即可, 如果不符合条件则抛出异常. 最后的实现效果如下:

2

报表布局(layout)

odoo在默认情况下支持4个报表布局,分别是:

  • *

而他们的都继承自基础布局(basic_layout)

results matching ""

    No results matching ""