构建模块

危险

本教程已过时。我们建议改为阅读 服务器框架入门。

警告

本教程需要 已安装 Odoo

启动/停止 Odoo 服务器

Odoo 采用客户端/服务器架构,其中客户端是通过 RPC 访问 Odoo 服务器的 Web 浏览器。

业务逻辑和扩展通常在服务器端实现,不过也可以在客户端添加支持性的客户端功能(例如交互式地图等新的数据展示方式)。

要启动服务器,只需在 shell 中调用命令 odoo-bin,必要时加上该文件的完整路径:

odoo-bin

在终端中按两次 Ctrl-C 即可停止服务器,或者终止相应的操作系统进程。

构建 Odoo 模块

服务器端和客户端扩展都打包为 模块,模块可以选择性地加载到 数据库 中。

Odoo 模块既可以为 Odoo 系统添加全新的业务逻辑,也可以修改和扩展现有业务逻辑:可以创建一个模块为 Odoo 的通用会计支持添加您所在国家/地区的会计规则,而下一个模块则添加对公交车队实时可视化的支持。

因此,Odoo 中的一切都始于模块、终于模块。

模块的组成

一个 Odoo 模块可以包含若干元素:

业务对象

这些资源以 Python 类声明,由 Odoo 根据其配置自动持久化

对象视图

定义业务对象的界面显示

数据文件

用于声明模型元数据的 XML 或 CSV 文件:

Web 控制器

处理来自 Web 浏览器的请求

静态 Web 数据

Web 界面或网站使用的图片、CSS 或 JavaScript 文件

模块结构

每个模块都是 模块目录 中的一个目录。模块目录通过 --addons-path 选项指定。

小技巧

大多数命令行选项也可以通过 配置文件 设置

Odoo 模块由其 清单文件 声明。

模块同时也是一个带有 __init__.py 文件的 Python 包,其中包含模块内各个 Python 文件的导入指令。

例如,如果模块只有一个 mymodule.py 文件,__init__.py 可能包含:

from . import mymodule

Odoo 提供了帮助创建新模块的机制,其中 odoo-bin 有一个子命令 scaffold,用于创建空模块:

$ odoo-bin scaffold <module name> <where to put it>

该命令会为您的模块创建一个子目录,并自动为模块创建一组标准文件。其中大多数文件只包含注释掉的代码或 XML。本教程会陆续说明其中大多数文件的用途。

练习

创建模块

使用上面的命令行创建一个空模块 Open Academy,并在 Odoo 中安装它。

对象关系映射

Odoo 的一个关键组件是 ORM 层。该层避免了手动编写大部分 SQL,并提供可扩展性和安全性服务2。

业务对象以继承 Model 的 Python 类声明,从而将其集成到自动持久化系统中。

模型可以在定义时通过设置若干属性来配置。最重要的属性是 _name,它是必需的,用于定义模型在 Odoo 系统中的名称。下面是一个最简的完整模型定义:

from odoo import models
class MinimalModel(models.Model):
    _name = 'test.model'

模型字段

字段用于定义模型可以存储什么以及存储在哪里。字段定义为模型类上的属性:

from odoo import models, fields

class LessMinimalModel(models.Model):
    _name = 'test.model2'

    name = fields.Char()

常用属性

与模型本身类似,字段也可以配置,方法是把配置属性作为参数传入:

name = fields.Char(required=True)

某些属性适用于所有字段,以下是最常用的几个:

string (unicode,默认值:字段名)

字段在界面中显示的标签(用户可见)。

required (bool,默认值:False)

如果为 True,该字段不能为空,它必须具有默认值,或者在创建记录时总是被赋值为某个值。

help (unicode,默认值:'')

长文本形式,在界面中为用户提供帮助工具提示。

index (bool,默认值:False)

请求 Odoo 在该列上创建 database index。

简单字段

字段大致分为两大类:”简单”字段是直接存储在模型表中的原子值,而”关系”字段则用于关联记录(同一模型或不同模型的记录)。

简单字段的例子有 Boolean、Date、Char。

保留字段

Odoo 会在所有模型中创建少量字段1。这些字段由系统管理,不应写入。如有用或必要,可以读取它们:

id (Id)

记录在其模型中的唯一标识符。

create_date (Datetime)

记录的创建日期。

create_uid (Many2one)

创建该记录的用户。

write_date (Datetime)

记录的最后修改日期。

write_uid (Many2one)

最后修改该记录的用户。

特殊字段

默认情况下,Odoo 还要求所有模型都具有 name 字段,以支持各种显示和搜索行为。用于这些用途的字段可以通过设置 _rec_name 来覆盖。

练习

定义模型

在 openacademy 模块中定义一个新的数据模型 Course。课程具有标题和描述。课程必须有标题。

数据文件

Odoo 是一个高度数据驱动的系统。虽然行为是通过 Python 代码定制的,但模块的一部分价值在于它在加载时所设置的数据。

小技巧

有些模块的存在仅仅是为了向 Odoo 添加数据

模块数据通过 数据文件 声明,即包含 <record> 元素的 XML 文件。每个 <record> 元素都会创建或更新一条数据库记录。

<odoo>

        <record model="{model name}" id="{record identifier}">
            <field name="{a field name}">{a value}</field>
        </record>

</odoo>
  • model 是该记录对应的 Odoo 模型名称。

  • id 是一个 外部标识符,它允许引用该记录(无需知道其在数据库中的标识符)。

  • <field> 元素的 name 是模型中字段的名称(例如 description)。其主体是该字段的值。

数据文件必须在清单文件中声明后才会被加载,它们可以声明在 'data' 列表(始终加载)中,也可以声明在 'demo' 列表(仅在演示模式下加载)中。

练习

定义演示数据

创建演示数据,用几门演示课程填充 Courses 模型。

小技巧

数据文件的内容仅在模块安装或更新时才会加载。

做出一些更改后,别忘了使用 odoo-bin -u openacademy 将更改保存到数据库。

动作与菜单

动作和菜单是数据库中的普通记录,通常通过数据文件声明。动作可以通过三种方式触发:

  1. 通过点击菜单项(链接到特定动作)

  2. 通过点击视图中的按钮(如果这些按钮与动作相连)

  3. 作为对象上的上下文动作

由于菜单的声明较为复杂,因此提供了 <menuitem> 快捷方式,以便更轻松地声明 ir.ui.menu 并将其连接到相应的动作。

<record model="ir.actions.act_window" id="action_list_ideas">
    <field name="name">Ideas</field>
    <field name="res_model">idea.idea</field>
    <field name="view_mode">list,form</field>
</record>
<menuitem id="menu_ideas" parent="menu_root" name="Ideas" sequence="10"
          action="action_list_ideas"/>

危险

在 XML 文件中,动作必须声明在其对应菜单之前。

数据文件按顺序执行,因此在创建菜单之前,动作的 id 必须已存在于数据库中。

练习

定义新的菜单项

在 OpenAcademy 菜单项下定义新的菜单项以访问课程。用户应当能够:

  • 显示所有课程的列表

  • 创建/修改课程

基本视图

视图定义了模型中记录的显示方式。每种类型的视图代表一种可视化模式(记录列表、记录的聚合图表……)。视图既可以通过其类型通用地请求(例如 合作伙伴列表),也可以通过其 id 专门请求。对于通用请求,将使用类型正确且优先级最低的视图(因此每种类型中优先级最低的视图就是该类型的默认视图)。

视图继承 允许修改在其他地方声明的视图(添加或删除内容)。

通用视图声明

视图被声明为 ir.ui.view 模型的一条记录。视图类型由 arch 字段的根元素决定:

<record model="ir.ui.view" id="view_id">
    <field name="name">view.name</field>
    <field name="model">object_name</field>
    <field name="priority" eval="16"/>
    <field name="arch" type="xml">
        <!-- view content: <form>, <list>, <graph>, ... -->
    </field>
</record>

危险

视图的内容是 XML。

因此,arch 字段必须声明为 type="xml" 才能被正确解析。

列表视图

列表视图(list views)以表格形式显示记录。

其根元素是 <list>。列表视图最简单的形式就是列出要在表格中显示的所有字段(每个字段作为一列):

<list string="Idea list">
    <field name="name"/>
    <field name="inventor_id"/>
</list>

表单视图

表单用于创建和编辑单条记录。

其根元素是 <form>。它们由高层结构元素(分组、笔记本)和交互元素(按钮和字段)组成:

<form string="Idea form">
    <group colspan="4">
        <group colspan="2" col="2">
            <separator string="General stuff" colspan="2"/>
            <field name="name"/>
            <field name="inventor_id"/>
        </group>

        <group colspan="2" col="2">
            <separator string="Dates" colspan="2"/>
            <field name="active"/>
            <field name="invent_date" readonly="1"/>
        </group>

        <notebook colspan="4">
            <page string="Description">
                <field name="description" nolabel="1"/>
            </page>
        </notebook>

        <field name="state"/>
    </group>
</form>

练习

使用 XML 自定义表单视图

为 Course 对象创建你自己的表单视图。显示的数据应为:课程的名称和描述。

练习

笔记本

在 Course 表单视图中,将 description 字段放在一个标签页下,以便日后更容易地添加包含附加信息的其他标签页。

表单视图也可以使用纯 HTML 实现更灵活的布局:

<form string="Idea Form">
    <header>
        <button string="Confirm" type="object" name="action_confirm"
                invisible="state != 'draft'" class="oe_highlight" />
        <button string="Mark as done" type="object" name="action_done"
                invisible="state != 'confirmed'" class="oe_highlight"/>
        <button string="Reset to draft" type="object" name="action_draft"
                invisible="state not in ['confirmed', 'done']" />
        <field name="state" widget="statusbar"/>
    </header>
    <sheet>
        <div class="oe_title">
            <label for="name" class="oe_edit_only" string="Idea Name" />
            <h1><field name="name" /></h1>
        </div>
        <separator string="General" colspan="2" />
        <group colspan="2" col="2">
            <field name="description" placeholder="Idea description..." />
        </group>
    </sheet>
</form>

搜索视图

搜索视图用于自定义与列表视图(以及其他聚合视图)关联的搜索字段。其根元素是 <search>,由一些字段组成,这些字段定义了可以搜索哪些字段:

<search>
    <field name="name"/>
    <field name="inventor_id"/>
</search>

如果模型不存在搜索视图,Odoo 会生成一个仅允许搜索 name 字段的搜索视图。

练习

搜索课程

允许根据课程的标题或描述搜索课程。

模型之间的关系

一个模型中的记录可以与另一个模型中的记录相关联。例如,销售订单记录与包含客户数据的客户记录相关联;它还与其销售订单行记录相关联。

练习

创建会话模型

对于 Open Academy 模块,我们考虑为 会话 建立一个模型:会话是课程在特定时间为特定受众授课的一次实例。

为 会话 创建一个模型。会话具有名称、开始日期、持续时间和座位数。添加一个动作和一个菜单项来显示它们。通过菜单项使新模型可见。

关系字段

关系字段用于链接记录,既可以是同一模型内的记录(层级结构),也可以是不同模型之间的记录。

关系字段的类型有:

Many2one(other_model, ondelete='set null')

指向另一个对象的简单链接:

print(foo.other_id.name)

更多内容

外键

One2many(other_model, related_field)

虚拟关系,是 Many2one 的反向关系。One2many 的行为类似于记录的容器,访问它会得到一个(可能为空的)记录集:

for other in foo.other_ids:
    print(other.name)

危险

由于 One2many 是虚拟关系,因此在 other_model 中 必须 存在一个 Many2one 字段,且其名称 必须 为 related_field

Many2many(other_model)

双向多对多关系,一侧的任意记录都可以与另一侧任意数量的记录相关联。其行为类似于记录的容器,访问它同样会得到一个可能为空的记录集:

for other in foo.other_ids:
    print(other.name)

练习

many2one 关系

使用 many2one,修改 Course 和 Session 模型,以反映它们与其他模型的关系:

  • 课程有一个 负责 用户;该字段的值是内置模型 res.users 的一条记录。

  • 会话有一个 讲师;该字段的值是内置模型 res.partner 的一条记录。

  • 会话与一个 课程 相关联;该字段的值是模型 openacademy.course 的一条记录,且为必填项。

  • 调整视图。

练习

反向 one2many 关系

使用反向关系字段 one2many,修改模型以反映课程与会话之间的关系。

练习

多个 many2many 关系

使用关系字段 many2many,修改 Session 模型,将每个会话与一组 参与者 相关联。参与者将由合作伙伴记录表示,因此我们将关联到内置模型 res.partner。相应地调整视图。

继承

模型继承

Odoo 提供了两种 继承 机制,以模块化的方式扩展现有模型。

第一种继承机制允许一个模块修改在另一个模块中定义的模型的行为:

  • 向模型添加字段,

  • 覆盖模型中字段的定义,

  • 向模型添加约束,

  • 向模型添加方法,

  • 覆盖模型中已有的方法。

第二种继承机制(委托)允许将模型的每条记录链接到父模型中的一条记录,并提供对父记录字段的透明访问。

../../_images/inheritance_methods.png

更多内容

  • _inherit

  • _inherits

视图继承

Odoo 并不就地修改现有视图(通过覆盖它们),而是提供视图继承:子“扩展”视图叠加在根视图之上,并可以对其父视图添加或删除内容。

扩展视图使用 inherit_id 字段引用其父视图,其 arch 字段不再是单个视图,而是由任意数量的 xpath 元素组成,这些元素用于选择和修改其父视图的内容:

<!-- improved idea categories list -->
<record id="idea_category_list2" model="ir.ui.view">
    <field name="name">id.category.list2</field>
    <field name="model">idea.category</field>
    <field name="inherit_id" ref="id_category_list"/>
    <field name="arch" type="xml">
        <!-- find field description and add the field
             idea_ids after it -->
        <xpath expr="//field[@name='description']" position="after">
          <field name="idea_ids" string="Number of ideas"/>
        </xpath>
    </field>
</record>
expr

一个 XPath 表达式,用于在父视图中选择单个元素。如果未匹配任何元素或匹配到多个元素,则引发错误

position

要对匹配元素执行的操作:

inside

将 xpath 的主体附加到匹配元素的末尾

replace

用 xpath 的主体替换匹配元素,并将新主体中出现的所有 $0 节点替换为原始元素

before

将 xpath 的主体作为同级元素插入到匹配元素之前

after

将 xpaths 的主体作为同级元素插入到匹配元素之后

attributes

使用 xpath 主体中的特殊 attribute 元素修改匹配元素的属性

小技巧

在匹配单个元素时,可以直接在要查找的元素上设置 position 属性。下面两种继承方式将得到相同的结果。

<xpath expr="//field[@name='description']" position="after">
    <field name="idea_ids" />
</xpath>

<field name="description" position="after">
    <field name="idea_ids" />
</field>

练习

修改现有内容

  • 使用模型继承,修改现有的 Partner 模型,添加一个 instructor 布尔字段,以及一个对应会话-合作伙伴关系的 many2many 字段

  • 使用视图继承,在合作伙伴表单视图中显示这些字段

域

在 Odoo 中,域 是对记录条件进行编码的值。域是一个条件列表,用于选择模型记录的子集。每个条件由字段名、运算符和值组成一个三元组。

例如,在 Product 模型上使用时,以下域会选择所有单价超过 1000 的 服务:

[('product_type', '=', 'service'), ('unit_price', '>', 1000)]

默认情况下,条件以隐式 AND 组合。可以使用逻辑运算符 & (AND)、| (OR)和 ! (NOT)显式组合条件。它们使用前缀位置(运算符插入在其参数之前,而不是之间)。例如,要选择“属于 服务 或 单价 不在 1000 到 2000 之间的产品”:

['|',
    ('product_type', '=', 'service'),
    '!', '&',
        ('unit_price', '>=', 1000),
        ('unit_price', '<', 2000)]

可以为关系字段添加 domain 参数,以在客户端界面中选择记录时限制该关系的有效记录。

练习

关系字段上的域

在为 Session 选择讲师时,应只显示讲师(将 instructor 设为 True 的合作伙伴)。

练习

更复杂的域

创建新的合作伙伴类别 Teacher / Level 1 和 Teacher / Level 2。会话的讲师可以是讲师,也可以是教师(任意级别)。

计算字段与默认值

到目前为止,字段都是直接存储在数据库中并直接从数据库读取的。字段也可以是 计算 的。在这种情况下,字段值不是从数据库读取的,而是通过调用模型的方法即时计算出来的。

要创建计算字段,请创建一个字段并将其 compute 属性设置为方法名。计算方法只需为 self 中的每条记录设置要计算的字段值即可。

危险

self 是一个集合

self 对象是一个 记录集,即有序的记录集合。它支持 Python 对集合的标准操作,例如 len(self) 和 iter(self),以及 recs1 + recs2 等额外的集合运算。

遍历 self 会逐个返回记录,其中每条记录本身都是一个大小为 1 的集合。你可以使用点号表示法访问/赋值单条记录的字段,例如 record.name。

import random
from odoo import models, fields, api

class ComputedModel(models.Model):
    _name = 'test.computed'

    name = fields.Char(compute='_compute_name')

    def _compute_name(self):
        for record in self:
            record.name = str(random.randint(1, 1e6))

依赖

计算字段的值通常取决于被计算记录上其他字段的值。ORM 期望开发者在计算方法上使用装饰器 depends() 来指定这些依赖。当字段的某些依赖被修改时,ORM 会使用给定的依赖触发该字段的重新计算:

from odoo import models, fields, api

class ComputedModel(models.Model):
    _name = 'test.computed'

    name = fields.Char(compute='_compute_name')
    value = fields.Integer()

    @api.depends('value')
    def _compute_name(self):
        for record in self:
            record.name = "Record with value %s" % record.value

练习

计算字段

  • 向 Session 模型添加已占用座位的百分比

  • 在列表视图和表单视图中显示该字段

  • 将该字段显示为进度条

默认值

任何字段都可以设置默认值。在字段定义中,添加选项 default=X,其中 X 可以是 Python 字面值(布尔值、整数、浮点数、字符串),也可以是一个接受记录集并返回值的函数:

name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)

注解

self.env 对象提供了对请求参数及其他有用内容的访问:

  • self.env.cr 或 self._cr 是数据库 游标 对象,用于查询数据库

  • self.env.uid 或 self._uid 是当前用户的数据库 id

  • self.env.user 是当前用户的记录

  • self.env.context 或 self._context 是上下文字典

  • self.env.ref(xml_id) 返回 XML id 对应的记录

  • self.env[model_name] 返回给定模型的实例

练习

活跃对象 – 默认值

  • 将 start_date 的默认值定义为今天(参见 Date)。

  • 在 Session 类中添加字段 active,并默认将场次设为活跃。

Onchange 机制

“onchange” 机制为客户端界面提供了一种方式:每当用户在某字段中填入值后即可更新表单,而无需向数据库保存任何内容。

例如,假设某个模型有三个字段 amount、unit_price 和 price,而你希望在其它任一字段被修改时更新表单上的价格。为此,可定义一个方法,其中 self 表示表单视图中的记录,并用 onchange() 装饰它,以指定在哪个字段上触发。你对 self 所做的任何修改都会反映到表单上。

<!-- content of form view -->
<field name="amount"/>
<field name="unit_price"/>
<field name="price" readonly="1"/>
# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
    # set auto-changing field
    self.price = self.amount * self.unit_price
    # Can optionally return a warning and domains
    return {
        'warning': {
            'title': "Something bad happened",
            'message': "It was very bad indeed",
        }
    }

对于计算字段,带值的 onchange 行为是内置的,这一点通过试用 Session 表单即可看到:修改座位数或参与人数,taken_seats 进度条就会自动更新。

练习

警告

添加一个显式的 onchange,以对无效值发出警告,例如负数座位数,或参与人数多于座位数。

模型约束

Odoo 提供两种设置自动校验不变量的方式:Python 约束 和 SQL 约束。类似地,你还可以添加更复杂的 SQL 索引。

Python 约束定义为一个用 constrains() 装饰的方法,并在记录集上调用。装饰器指定该约束涉及哪些字段,以便其中一个字段被修改时自动求值该约束。若不变量不满足,该方法应抛出异常:

from odoo.exceptions import ValidationError

@api.constrains('age')
def _check_something(self):
    for record in self:
        if record.age > 20:
            raise ValidationError("Your record is too old: %s" % record.age)
    # all records passed the test, don't return anything

练习

添加 Python 约束

添加一个约束,检查讲师不出现在其本人场次的参与者之中。

约束和索引通过以下方式定义:Constraint、Index 和 UniqueIndex。

练习

添加 SQL 约束

借助 PostgreSQL 文档,添加以下约束:

  1. CHECK:检查课程描述与课程标题不相同

  2. 将 Course 的名称设为 UNIQUE

练习

练习 6 - 添加复制选项

由于我们为 Course 名称添加了唯一性约束,现已无法再使用 “duplicate” 功能(可通过 表单 ‣ 复制 操作)。

重新实现你自己的 “copy” 方法,使 Course 对象可以被复制,并将原名称改为 “Copy of [original name]”。

高级视图

列表视图

列表视图还可以接受附加属性,以进一步自定义其行为:

decoration-{$name}

允许根据对应记录的属性更改某一行文本的样式。

这些值是 Python 表达式。对于每条记录,会以记录的属性作为上下文值对该表达式求值,如果结果为 true,则将相应样式应用到该行。以下是上下文中可用的其他一些值:

  • uid:当前用户的 id,

  • today:当前本地日期,为 YYYY-MM-DD 形式的字符串,

  • now:与 today 相同,但包含当前时间。该值的格式为 YYYY-MM-DD hh:mm:ss。

{$name} 可以是 bf (font-weight: bold)、it (font-style: italic),或任意 Bootstrap 上下文颜色 (danger、info、muted、primary、success 或 warning)。

<list string="Idea Categories" decoration-info="state=='draft'"
    decoration-danger="state=='trashed'">
    <field name="name"/>
    <field name="state"/>
</list>
editable

取值为 "top" 或 "bottom"。使列表视图可就地编辑(无需经过表单视图),该值指定新行出现的位置。

练习

列表着色

修改 Session 列表视图,使持续少于 5 天的场次显示为蓝色,持续多于 15 天的场次显示为红色。

日历视图

将记录显示为日历事件。其根元素为 <calendar>,最常用的属性如下:

color

用于 颜色分段 的字段名。颜色会自动分配给事件,但同一颜色分段中的事件(即 @color 字段值相同的记录)会被赋予相同的颜色。

date_start

保存事件开始日期/时间的记录字段

date_stop (可选)

保存事件结束日期/时间的记录字段

string

用于定义每个日历事件标签的记录字段

<calendar string="Ideas" date_start="invent_date" color="inventor_id">
    <field name="name"/>
</calendar>

练习

日历视图

为 Session 模型添加日历视图,使用户可以查看与 Open Academy 关联的事件。

搜索视图

搜索视图的 <field> 元素可以带有 @filter_domain,用于覆盖针对给定字段生成的搜索域。在该域中,self 表示用户输入的值。在下面的示例中,它被用于同时搜索 name 和 description 两个字段。

搜索视图还可以包含 <filter> 元素,它们充当预定义搜索的开关。过滤器必须具有以下属性之一:

domain

将给定的域添加到当前搜索

context

为当前搜索添加一些上下文;使用键 group_by 按给定字段名对结果分组

<search string="Ideas">
    <field name="name"/>
    <field name="description" string="Name and description"
           filter_domain="['|', ('name', 'ilike', self), ('description', 'ilike', self)]"/>
    <field name="inventor_id"/>
    <field name="country_id" widget="selection"/>

    <filter name="my_ideas" string="My Ideas"
            domain="[('inventor_id', '=', uid)]"/>
    <group string="Group By">
        <filter name="group_by_inventor" string="Inventor"
                context="{'group_by': 'inventor_id'}"/>
    </group>
</search>

要在动作中使用非默认搜索视图,需通过动作记录的 search_view_id 字段将其关联。

动作还可以通过其 context 字段为搜索字段设置默认值:形如 search_default_field_name 的上下文键会用所提供的值初始化 field_name。搜索过滤器必须带有可选的 @name,才能拥有默认值并表现为布尔型(它们只能默认启用)。

练习

搜索视图

  1. 在课程搜索视图中添加一个按钮,用于筛选出当前用户担任负责人的课程,并使其默认被选中。

  2. 添加一个按钮,按负责人对课程分组。

甘特图

警告

甘特视图需要 web_gantt 模块,该模块存在于 企业版 版本中。

横向条形图,通常用于展示项目计划与进度,其根元素为 <gantt>。

<gantt string="Ideas"
       date_start="invent_date"
       date_stop="date_finished"
       progress="progress"
       default_group_by="inventor_id" />

练习

甘特图

添加一个甘特图,使用户可以查看与 Open Academy 模块关联的场次安排。场次应按讲师分组。

图表视图

图表视图可对模型进行聚合概览与分析,其根元素为 <graph>。

注解

数据透视视图(元素 <pivot>)是一种多维表格,允许选择筛选条件和维度,以在转入更图形化的概览之前获得正确的聚合数据集。数据透视视图与图表视图共享相同的内容定义。

图表视图有 4 种显示模式,默认模式通过 @type 属性选择。

条形图(默认)

条形图,第一个维度用于在水平轴上定义分组,其它维度定义每个分组内的聚合条形。

默认情况下条形并排显示,可通过在 <graph> 上使用 @stacked="True" 将其堆叠。

行

二维折线图

派

二维饼图

图表视图包含 <field>,其 @type 属性为必填,可取以下值:

row (默认)

该字段默认应被聚合

measure

该字段应被聚合而不是用于分组

<graph string="Total idea score by Inventor">
    <field name="inventor_id"/>
    <field name="score" type="measure"/>
</graph>

警告

图表视图对数据库中的值执行聚合,因此不适用于非存储的计算字段。

练习

图形视图

在 Session 对象中添加一个图表视图,以条形图的形式显示每门课程的参与者人数。

看板

用于组织任务、生产流程等……其根元素为 <kanban>。

看板视图显示一组卡片,这些卡片可以按列分组。每张卡片代表一条记录,每一列代表某个聚合字段的取值。

例如,项目任务可以按阶段组织(每列是一个阶段),或按负责人组织(每列是一个用户),等等。

看板视图将每张卡片的结构定义为表单元素(包括基本 HTML)与 QWeb 模板 的混合。

练习

看板视图

添加一个看板视图,显示按课程分组的场次(因此每列就是一门课程)。

安全

必须配置访问控制机制,以实现一致的安全策略。

基于用户组的访问控制机制

用户组在 res.groups 模型上作为普通记录创建,并通过菜单定义授予菜单访问权。然而即使没有菜单,对象仍可能被间接访问,因此必须为用户组定义真正的对象级权限(读取、写入、创建、删除)。这些权限通常通过模块内的 CSV 文件插入。也可以使用字段的 groups 属性限制对视图或对象上特定字段的访问。

访问权限

访问权限定义为模型 ir.access 的记录。每条访问权限关联到一个模型、一个用户组(全局访问限制则不关联用户组)、一组操作(创建、读取、更新、删除)以及一个可选的域。此类访问权限通常由以模型命名的 CSV 文件创建:ir.access.csv。

id,name,model_id,group_id/id,operation,domain
access_idea_idea,idea.idea,model_idea_idea,base.group_user,cru,
access_idea_vote_read,idea.vote,model_idea_vote,base.group_user,r,
access_idea_vote_delete,idea.vote,model_idea_vote,base.group_user,d,"[('state', '=', 'cancel')]"
access_idea_vote_update,idea.vote,model_idea_vote,base.group_user,cu,"[('user_id', '=', user.id)]"

练习

通过 Odoo 界面添加访问控制

创建一个新用户 “John Smith”。然后创建一个用户组 “OpenAcademy / Session Read”,赋予其对 Session 模型的读取访问权限。

练习

通过模块中的数据文件添加访问控制

使用数据文件,

  • 创建一个用户组 OpenAcademy / Manager,拥有对所有 OpenAcademy 模型的完全访问权限

  • 使所有用户都能读取 Session 和 Course

访问权限记录授予对给定模型中一部分记录的访问权。其 域 指定了向哪些记录授予访问权限。

在上面的示例中,访问权限 access_idea_vote_delete 阻止删除状态不是 'cancel' 的投票,而访问权限 access_idea_vote_update 阻止用户创建或更新不属于他们的投票。

练习

访问权限的域

为 Course 模型和用户组 “OpenAcademy / Manager” 修改一条访问权限,将 write 和 unlink 访问权限限制给课程的负责人。如果课程没有负责人,则该用户组的所有用户都必须能够修改它。

向导

向导通过动态表单描述与用户的交互会话(或对话框)。向导只是一个继承 TransientModel 而不是 Model 的模型。TransientModel 类继承 Model,并复用其所有现有机制,但具有以下特点:

  • 向导记录并非用于持久保存;它们会在一定时间后自动从数据库中删除。这就是它们被称为 transient (瞬态)的原因。

  • 向导记录可以通过关系字段(many2one 或 many2many)引用常规记录或向导记录,但常规记录 不能 通过 many2one 字段引用向导记录。

我们希望创建一个向导,使用户可以为某个特定场次或一次性为一组场次创建参与者。

练习

定义向导

创建一个向导模型,与 Session 模型建立 many2one 关系,与 Partner 模型建立 many2many 关系。

启动向导

向导其实只是 target 字段值为 new 的 窗口动作,它会在单独的对话框中打开视图(通常是 表单)。该动作可以通过菜单项触发,但更常见的是通过按钮触发。

启动向导的另一种方式是通过列表视图或表单视图的 动作 菜单。这是通过动作的 binding_model_id 字段实现的。设置该字段后,动作会显示在其所”绑定”的模型的视图上。

<record id="launch_the_wizard" model="ir.actions.act_window">
    <field name="name">Launch the Wizard</field>
    <field name="res_model">wizard.model.name</field>
    <field name="view_mode">form</field>
    <field name="target">new</field>
    <field name="binding_model_id" ref="model_context_model_ref"/>
</record>

小技巧

虽然向导使用常规视图和按钮,但通常点击表单中的任何按钮都会先保存表单再关闭对话框。由于这在向导中往往并不合适,因此提供了特殊属性 special="cancel",可立即关闭向导而不保存表单。

练习

启动向导

  1. 为向导定义一个表单视图。

  2. 添加动作,以便在 Session 模型的上下文中启动它。

  3. 为向导中的 session 字段定义默认值;使用上下文参数 self._context 获取当前场次。

练习

登记参与者

为向导添加按钮,并实现相应的方法,将参与者添加到指定场次。

练习

将参与者登记到多个场次

修改向导模型,使参与者可以登记到多个场次。

国际化

每个模块都可以在 i18n 目录中提供自己的翻译,方式是使用名为 LANG.po 的文件,其中 LANG 是该语言的语言区域代码,或在两者不同时使用语言与国家组合(例如 pt.po 或 pt_BR.po)。Odoo 会为所有启用的语言自动加载这些翻译。开发者在创建模块时始终使用英文,然后使用 Odoo 的 gettext POT 导出功能(设置 ‣ 翻译 ‣ 导入/导出 ‣ 导出翻译,不指定语言)导出模块术语,以创建模块模板 POT 文件,再据此衍生出翻译后的 PO 文件。许多 IDE 都有用于编辑和合并 PO/POT 文件的插件或模式。

小技巧

Odoo 生成的 Portable Object 文件发布在 Odoo 翻译平台 上,使软件翻译变得简单。

|- idea/ # The module directory
   |- i18n/ # Translation files
      | - idea.pot # Translation Template (exported from Odoo)
      | - fr.po # French translation
      | - pt_BR.po # Brazilian Portuguese translation
      | (...)

小技巧

默认情况下,Odoo 的 POT 导出只提取 XML 文件内的标签或 Python 代码字段定义内的标签,但任何 Python 字符串都可以通过用 odoo._() 函数包裹它以这种方式翻译(例如 _("Label"))

练习

翻译模块

为你的 Odoo 安装选择第二种语言。使用 Odoo 提供的功能翻译你的模块。

报表统计

打印报表

Odoo 使用的报表引擎基于 QWeb 模板、Twitter Bootstrap 和 Wkhtmltopdf。

报表由两个元素组合而成:

  • 一个 ir.actions.report,用于配置报表的各种基本参数(默认类型、报表生成后是否保存到数据库等)

    <record id="account_invoices" model="ir.actions.report">
        <field name="name">Invoices</field>
        <field name="model">account.invoice</field>
        <field name="report_type">qweb-pdf</field>
        <field name="report_name">account.report_invoice</field>
        <field name="report_file">account.report_invoice</field>
        <field name="attachment_use" eval="True"/>
        <field name="attachment">(object.state in ('open','paid')) and
            ('INV'+(object.number or '').replace('/','')+'.pdf')</field>
        <field name="binding_model_id" ref="model_account_invoice"/>
        <field name="binding_type">report</field>
    </record>
    

    小技巧

    由于它基本上是一个标准动作,与 向导 一样,通常通过 binding_model_id 字段将报表作为 上下文菜单项 添加到被报表模型的列表视图和/或表单视图上会很有用。

    这里我们还使用了 binding_type,以便报表出现在 report 上下文菜单中,而不是 action 菜单中。二者在技术上没有差异,但把元素放在正确的位置有助于用户。

  • 用于实际报表的标准 QWeb 视图:

    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="o">
            <t t-call="web.external_layout">
                <div class="page">
                    <h2>Report title</h2>
                </div>
            </t>
        </t>
    </t>
    

    标准渲染上下文提供了若干元素,其中最重要的有:

    docs

    为其打印报表的记录

    user

    打印报表的用户

由于报表是标准网页,因此可以通过 URL 访问,并且输出参数也可以通过该 URL 操控,例如 Invoice 报表的 HTML 版本可通过 http://localhost:8069/report/html/account.report_invoice/1 访问(如果已安装 account),PDF 版本则可通过 http://localhost:8069/report/pdf/account.report_invoice/1 访问。

危险

如果发现你的 PDF 报表缺少样式(即文本出现了,但样式/布局与 HTML 版本不同),很可能是你的 wkhtmltopdf 进程无法访问 Web 服务器来下载这些样式。

如果你检查服务器日志,发现在生成 PDF 报表时 CSS 样式没有被下载,那么很可能就是这个原因。

wkhtmltopdf 进程会将 web.base.url 系统参数用作所有链接文件的 根路径,但每次管理员登录时该参数都会被自动更新。如果你的服务器位于某种代理之后,该地址可能无法访问。你可以通过添加以下系统参数之一来解决:

  • report.url,指向一个可从你的服务器访问的 URL(可能是 http://localhost:8069 或类似地址)。它仅用于此特定目的。

  • web.base.url.freeze,设为 True 时,将停止对 web.base.url 的自动更新。

练习

为 Session 模型创建报表

对于每个会话,它应显示会话的名称、开始和结束时间,并列出该会话的参与者。

仪表板

练习

定义仪表板

定义一个仪表板,其中包含你创建的图表视图、会话日历视图以及课程列表视图(可切换到表单视图)。该仪表板应可通过菜单中的菜单项访问,并在选中 OpenAcademy 主菜单时自动显示在 Web 客户端中。

1

可以 禁用某些字段的自动创建

2

编写原生 SQL 查询是可行的,但需要谨慎,因为它会绕过 Odoo 的所有身份验证和安全机制。