第六章 Odoo 12开发之模型 – 结构化应用数据

本文为最好用的免费ERP系统Odoo 12开发手册系列文章第六篇。

在本系列文章第三篇Odoo 12 开发之创建第一个 Odoo 应用中,我们概览了创建 Odoo 应用所需的所有组件。本文及接下来的一篇我们将深入到组成应用的每一层:模型层、视图层和业务逻辑层。

本文中我们将深入学习模型层,以及学习如何使用模型来设计应用所需的数据结构。我们会探索模型和字段的各项作用,包括定义模型关系、添加计算字段、创建数据约束。

本文的主要内容有:

  • 学习项目 - 优化图书馆应用
  • 创建模型
  • 创建字段
  • 模型间的关系
  • 计算字段
  • 模型约束
  • 了解 Odoo的 base 模型

开发准备

本文代码基于第三章 Odoo 12 开发之创建第一个 Odoo 应用中所创建的代码。相关代码参见 GitHub 仓库,本文学习完成项目请参见GitHub 仓库。相关代码需放在一个 addons 路径中,然后在 Odoo中安装了 library_app 模型,本文中例子将会对该模块修改和新增代码。

学习项目 - 优化图书应用

在第三章 Odoo 12 开发之创建第一个 Odoo 应用中,我们创建了一个library_app插件模块,实现了一个简单的library.book模型用于展示图书目录。本文中,我们将回到该模块来丰富图书数据。我们将添加一个分类层级,添加如下用作图书分类:

  • Name:分类标题
  • Parent:所属父级分类
  • Subcategories:将此作为父级分类的子分类
  • Featured book或author: 此分类中所选图书或作者

图书模型中已有一些基本信息字段,我们会添加一些字段来展示 Odoo中的数据类型。我们还会为图书模型添加一些约束:

  • 标题和出版日期应唯一
  • 输入的ISBN应为有效

创建模型

模型是 Odoo 框架的核心,它们描述应用的数据结构,是应用服务和数据库存储之间的桥梁。可围绕模型实现业务逻辑来为应用添加功能,用户界面也建立在模型之上。下面我们将学习模型的通用属性,用于影响行为,以及几种模型类型:普通(regular)、临时(transient)和抽象(abstract)类型。

模型属性

模型类可以使用控制其部分行为的额外属性,以下是最常用的属性:

  • _name 是我们创建的 Odoo 模型的内部标识符,在创建新模型时为必填。
  • _description是对用户友好的模块记录标题,在用户界面中查看模型时显示。可选但推荐添加。
  • _order设置浏览模型记录时或列表视图的默认排序。其值为 SQL 语句中 order by 使用的字符串,所以可以传入符合 SQL 语法的任意值,它有智能模式并支持可翻译及many-to-one字段名。

我们的图书模型中已使用了_name 和_description属性,可以添加一个_order属性来默认以图书名排序,然后按出版日期倒序排(新出版在前)。

1
2
3
4
class Book(models.Model):
_name = 'library.book'
_description = 'Book'
_order = 'name, date_published desc'

在高级用例中还会用到如下属性:

  • _rec_name在从关联字段(如many-to-one关联)中引用时作为记录描述。默认使用模型中常用的 name字段,但可以指定任意其它字段。
  • _table是模型对应的数据表名。默认表名由 ORM 通过替换模块名中的点为下划线来自动定义,但是可通过该属性指定表名。
  • _log_access=False用于设置不自动创建审计追踪字段:create_uid, create_date, write_uid和write_date。
  • _auto=False 用于设置不自动创建模型对应的数据表。如有需要,可通过重载init()方法来创建数据库对象:数据表或视图。

还有用于继承模块的_inherit和_inherits属性,在本文后续会深入学习。

模型和 Python 类

Odoo 模型以 Python 类的形式展现,在前面的代码中,有一个继承了 models.Model类的 Python 类:Book,创建了新 Odoo 模型:library.book。Odoo的模型保存在中央注册表(central registry)中,可通过 env 环境对象(老 API 中称为 pool)获取。 它是一个数据库保存所有可用模型类引用的字典,其中的词条可通过模型名引用 。具体来说,模型方法中的代码可使用self.env[‘library.book’]来获取表示 library.book模型的模型类。

可以看出模型名非常重要,因为它是访问该注册表的关键。模型名的规则是以点号连接的小写单词,如library.book或library.book.category。内核模块中的其它示例有project.project, project.task和project.task.type。模型名应使用单数,如library.book而非library.books。

ℹ️由于历史原因,有些内核模型没有遵循这一规则,如res.users。

模型名必须全局唯一,因此第一个单词应使用模块关联的主应用对应,以图书应用而言,模型名前缀使用 library。其它示例如内核模块的project, crm和sale。另一方面 Python 类仅为所声明文件本地内容,名称仅需在代码文件中唯一即可。因为类名不会与其它模块中的类产生冲突,也就不需为其添加主应用相关的前缀。

类的命名规范是使用驼峰命名法(CamelCase),这与 Python 标准的 PEP8编码规范一致。

临时(Transient)模型和抽象模型

在前述代码中以及在大多数据 Odoo 模型中的类会继承models.Model类。这类模型在数据库中持久化存储:会为模型创建数据表并存储记录直至删除。但 Odoo 中还有另外两种模型类型:临时模型和抽象模型。

临时模型继承models.TransientModel类,用于向导式的用户交互。这类数据会存储在数据库中,但仅是临时性的。会定时运行清空 job 来清除这些表中的老数据。比如Settings > Translations菜单下的Load a Language对话窗口,就使用了临时模型来存储用户选择并实现向导逻辑。在第八章 Odoo 12开发之业务逻辑 - 业务流程的支持中会有讨论临时模型的示例。

抽象模型继承models.AbstractModel类,它不带有数据存储。抽象模型用作可复用的功能集,与使用 Odoo 继承功能的其它模型配合使用。例如mail.thread是 Discuss 应用中的一个抽象模型,用于为其它模型添加消息和follower 功能。

检查已有模型

通过 Python 类创建的模型和字段在用户界面中有自己的元标签。启动开发者模式,访问菜单Settings > Technical > Database Structure > Models,这里有数据库中的所有模型。点击列表中的模型会打开详情表单:

Odoo 12图书模型

这是一个检查模型结构很好的工具,因为在这里可以看到不同模块所有自定义结果。上图中在右上角 In Apps字段中可以看到library.book模型的定义来自library_app和library_member两个模块。下方区域中还有几个包含附加信息的标签:

  • Fields可快速查看模型字段
  • Access Rights是授予不同权限组的访问控制规则
  • Views显示模型所带的视图列表

我们可以通过开发者菜单下的View Metadata选项查看模型的外部标识符。模型的外部标识符或XML ID由 ORM 自动生成,但根据规则可预知,如library.book模型的外部标识符为model_library_book。在定义安全访问控制列表经常在 CSV 文件中使用到这些XML ID。

小贴士: 如第一章 使用开发者模式快速入门 Odoo 12中所见,模型表单是可编辑的。通过这里是可以创建并修改模型、字段和视图的。可在此处创建原型然后在插件模块中实现。

创建字段

创建新模型后的第一步是添加字段。Odoo 支持我们能想到的所有基本数据类型,如文本字符串、整型、浮点型、布尔型、日期、日期时间以及图片或二进制数据。下面就来看看 Odoo 中一些可用的字段类型吧。

基本字段类型

我们将为图书模型添加几种可用的字段类型,编辑library_app/models/library_book.py文件后 Book 类会长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Book(models.Model):
...
# String fields
name = fields.Char('Title', required=True)
isbn = fields.Char('ISBN')
book_type = fields.Selection(
[('paper', 'Paperback'),
('hard', 'Hardcover'),
('electronic', 'Electronic'),
('other', 'Other')],
'Type')
notes = fields.Text('Internal Notes')
descr = fields.Html('Description')

# Numeric fields:
copies = fields.Integer(default=1)
avg_rating = fields.Float('Average Rating', (3,2))
price = fields.Monetary('Price', 'currency_id')
currency_id = fields.Many2one('res.currency') # price helper

# Date and time fields
date_published = fields.Date()
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default=lambda self: fields.Datetime.now())

# Other fields
active = fields.Boolean('Active?', default=True)
image = fields.Binary('Cover')

# Relational Fields
...

此处是 Odoo 中所带的非关联字段示例,每个字段都带有所需的位置参数。

ℹ️Python 中有两类参数:位置参数和关键字参数。位置参数需按指定顺序使用。例如,f(x, y)应以f(1, 2)方式调用。关键字参数通过参数名传递。如同一个例子,可使用f(x=1, y=2)甚至是f(1, y=2)两种传参方式混用。更多有关关键字参数知识参见 Python 官方文档

对于大多数非关联字段,第一个参数是字段标题,与字符串字段参数相对应。它用作用户界面标签的默认文本。这个参数是可选的,如未传入,会根据字段名将下划线替换为空格并将单词首字母大写来自动生成。以下为可用的非关联字段类型以及其对应的位置参数:

  • Char(string)是一个单行文本,唯一位置参数是string字段标签。
  • Text(string)是一个多行文本,唯一位置参数是string字段标签。
  • Selection(selection, string)是一个下拉选择列表。选项位置参数是一个[(‘value’, ‘Title’),]元组列表。元组第一个元素是存储在数据库中的值,第二个元素是展示在用户界面中的描述。该列表可由其它模块使用selection_add关键字参数扩展。
  • Html(string)存储为文本字段,但有针对用户界面 HTML 内容展示的特殊处理。出于安全考虑,该字段会被清洗,但清洗行为可被重载。
  • Integer(string)仅需字段标题字符串参数。
  • Float(string, digits)带有第二个可选参数digits,该字段是一个指定字段精度的(x,y)元组,x 是数字总长,y 是小数位。
  • Monetary(string, currency_field)与浮点字段类似,但带有货币的特殊处理。第二个参数currency_field用于存储所使用货币,默认应传入currency_id字段。
  • Date(string)和Datetime(string)字段只需一个字符串文本位置参数。
  • Boolean(string)的值为True 或False,可传入一个字符串文本位置参数。
  • Binary(string)存储文件类二进制文件,只需一个字符串文本位置参数。它可由Python使用 base64编码字符串进行处理。

ℹ️Odoo 12中的修改
Date和Datetime 字段现在 ORM 中作为日期对象处理。此前的版本中作为文本字符串处理,进行操作时需与 Python 日期对象间进行转换。

文本字符串:Char, Text和Html有一些特有属性:

  • size (Char)设置最大允许尺寸。无特殊原因建议不要使用,例如可用于带有最大允许长度的社保账号。
  • translate使用得字段内容可翻译,带有针对不同语言的不同值。
  • trim默认值为 True,启动在网络客户端中自动去除周围的空格。可通过设置trim=false来取消。

ℹ️Odoo 12中的修改
trim字段属性在 Odoo 12中引入,此前版本中文本字段保存前后的空格。

除这些以外,还有在后面会介绍到的关联字段。不过, 我们还要先了解下有关字段属性的其它知识。

常用字段属性

字段还有一些其它属性供我们定义其行为。以下是常用的属性,通常都作为关键字参数:

  • string是字段的默认标签,在用户界面中使用。除Selection和关联字段外,它都是第一个位置参数,所以大多数情况下它用作关键字参数。如未传入,将由字段名自动生成。
  • default设置字段默认值。可以是具体值(如 active字段中的default=True),或是可调用引用,有名函数或匿名函数均可。
  • help提供 UI 中鼠标悬停字段向用户显示的提示文本。
  • readonly=True会使用户界面中的字段默认不可编辑。在 API 层面并没有强制,模型方法中的代码仍然可以向其写入。仅针对用户界面设置。
  • required=True使得用户界面中字段默认必填。这通过在数据库层面为列添加NOT NULL 约束来实现。
  • index=True为字段添加数据库索引,让搜索更快速,但同时也会部分降低写操作速度。
  • copy=False让字段在使用 ORM copy()方法复制字段时忽略该字段。除 to-many 关联字段外,其它字段值默认会被复制。
  • groups可限制字段仅对一些组可访问并可见。值为逗号分隔的安全组XML ID列表,如groups=’base.group_user,base.group_system’。
  • states传入依赖 state字段值的 UI 属性的字典映射值。可用属性有readonly, required和invisible,例如states={‘done’:[(‘readonly’,True)]}。

ℹ️注意states 字段等价于视图中的 attrs 属性。同时注意视图也支持 states 属性,但用途不同,传入逗号分隔的状态列表来控制元素什么时候可见。

以下为字段属性关键字参数的使用示例:

1
2
3
4
5
6
7
8
9
name = fields.Char(
'Title',
default=None,
index=True,
help='Book cover title',
readonly=False,
required=True,
translate=False,
)

如前所述,default 属性可带有固定值,或引用函数来自动计算默认值。对于简单运算,可使用 lambda 函数来避免过重的有名函数或方法的创建。以下是一个计算当前日期和时间默认值的常用示例:

1
2
3
4
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default=lambda self: fields.Datetime.now(),
)

默认值也可以是一个函数引用,或待定义函数名字符串:

1
2
3
4
5
6
7
last_borrow_date = fields.Datetime(
'Last Borrowed On',
default='_default_last_borrow_date',
)

def _default_last_borrow_date(self):
return fields.Datetime.now()

当模块数据结构在不同版本中变更时以下两个属性非常有用:

  • deprecated=True在字段被使用时记录一条 warning 日志
  • oldname=’field’是在新版本中重命名字段时使用,可在升级模块时将老字段中的数据自动拷贝到新字段中

特殊字段名

一些字段名很特别,可能是因为它们出于特殊目的作为 ORM 保留字,或者是由于内置功能使用了一些默认字段名。id 字段保留以用作标识每条记录的自增数字以及数据库主键,每个模型都会自动添加。

以下字段只要模型中没设置_log_access=False都会在新模型中自动创建:

  • create_uid为创建记录的用户
  • create_date是记录创建的日期和时间
  • write_uid是最后写入记录的用户
  • write_date是最后修改记录的日期和时间

每条记录的这些字段信息都可通过开发者菜单下的View Metadata进行查看。一些内置 API 功能默认需要一些指定字段名。避免在不必要的场合使用这些字段名会让开发更轻松。其中有些字段名被保留并且不能在其它地方使用:

  • name (通常为 Char)默认作为记录的显示名称。通过是一个 Char,但也可以是 Text 或Many2one字段类型。用作显示名的字段可修改为_rec_name模型属性。
  • active (Boolean型)允许我们关闭记录。带有active=False的记录会自动从查询中排除掉。可在当前上下文中添加{‘active_test’: False} 来关闭这一自动过滤。可用作记录存档或假删除(soft delete)。
  • state (Selection类型) 表示记录生命周期的基本状态。它允许使用states字段属性来根据记录状态以具备不同的 UI 行为。动态修改视图:字段可在特定记录状态下变为readonly, required或invisible。
  • parent_id和parent_path Integer和Char型)对于父子层级关系具有特殊意义。本文后续会进行讨论。

ℹ️Odoo 12中的修改
层级关联现在使用parent_path字段,它替代了老版本中已淘汰的parent_left和 parent_right字段(整型)。

到目前为止我们讨论的都是非关联字段。但应用数据结构中很大一部分是描述实体间关联的。下面就一起来学习。

模型间的关系

中、大型业务应用有一个结构数据模型,需要关联所涉及到的不同实体间的数据。要实现这点,需要使用关联字段。再来看看我们的图书应用,图书模型中有如下关系:

  • 每本书有一个出版商。这是一个many-to-one 关联,在数据库引擎中通过外键实现。反过来则是one-to-many关联,表示一个出版商可出版多本书。
  • 每本书可以有多名作者。这是一个many-to-many关联,反过来还是many-to-many关联,因为一个作者也可以有多本书。

下面我们就会分别讨论这些关联。具体的用例就是层级关联,即一个模型中的记录与同模型中的其它记录关联。我们将引入一个图书分类模型解释这一情况。最后,Odoo 框架还支持弹性关系,即一个字段可指向其它表中的字段,这称为引用字段。

Many-to-one关联

many-to-one关联是对其它模型中记录的引用,例如在图书模型中,publisher_id表示图书出版商,是对partner记录的一个引用:

1
2
publisher_id = fields.Many2one(
'res.partner', string='Publisher')

与所有关联字段一样,Many2one字段的第一个位置参数是关联模型(comodel关键字参数)。第二位置参数是字段标签(string关键字参数),但这和其它关联字段不同,所以推荐使用像以上代码一样一直使用string关键字参数。

many-to-one模型字段在数据表中创建一个字段,并带有指向关联表的外键,其中为关联记录的数据库 ID。以下是many-to-one字段可用的关键字参数:

  • ondelete定义关联记录删除时执行的操作:

    • set null (默认值): 关联字段删除时会置为空值
    • restricted:抛出错误阻止删除
    • cascade:在关联记录删除时同时删除当前记录
  • context是一个数据字典,可在浏览关联时为网页客户端传递信息,比如设置默认值。第八章 Odoo 12开发之业务逻辑 - 业务流程的支持中会做深入说明。

  • domain是一个域表达式:使用一个元组列表过滤记录来作为关联记录选项,第八章 Odoo 12开发之业务逻辑 - 业务流程的支持中会详细说明。

  • auto_join=True允许ORM在使用关联进行搜索时使用SQL连接。使用时会跳过访问安全规则,用户可以访问安全规则不允许其访问的关联记录,但这样 SQL 的查询会更有效率且更快。

  • delegate=True 创建一个关联记录的代理继承。使用时必须设置required=True和ondelete=’cascade’。代理继承更多知识参见第四章 Odoo 12 开发之模块继承

One-to-many反向关联

one-to-many关联是many-to-one的反向关联。它列出引用该记录的关联模型记录。比如在图书模型中,publisher_id与 parnter 模型是一个many-to-one关联。这说明partner与图书模型可以有一个one-to-many的反向关联,列出每个出版商出版的图书。

要让关联可用,我们可在 partner 模型中添加它,在library_app/models/res_partner.py文件中添加如下代码:

1
2
3
4
5
6
7
8
from odoo import fields, models

class Partner(models.Model):
_inherit = 'res.partner'
published_book_ids = fields.One2many(
'library.book', # related model
'publisher_id', # fields for "this" on related model
string='Published Books')

我们向模块添加了新文件,所以不要忘记在library_app/models/init.py中导入该文件:

1
2
from . import library_book
from . import res_partner

One2many字段接收三个位置参数:

  • 关联模型 (comodel_name关键字参数)
  • 引用该记录的模型字段 (inverse_name关键字参数)
  • 字段标签 (string关键字参数)

其它可用的关键字参数与many-to-one字段相同:context, domain和ondelete(此处作用于关联中的 many 这一方)。

Many-to-many关联

在两端都存在to-many关联时使用many-to-many关联。还是以我们的图书应用为例,书和作者之间是many-to-many关联:一本书可以有多个作者,一个作者可以有多本书。图书端有一个library.book模型:

1
2
3
4
5
class Book(models.Model):
_name = 'library.book'
...
author_ids = fields.Many2many(
'res.partner', string='Authors')

在作者端,我们也可以为res.partner添加一个反向关联:

1
2
3
4
class Partner(models.Model):
_inherit = 'res.partner'
book_ids = fields.Many2many(
'library.book', string='Authored Books')

Many2many最少要包含一个关联模型位置参数(comodel_name关键字参数),推荐为字段标签提供一个string参数。

在数据库层面上,many-to-many关联不会在已有表中添加任何列。而是自动创建一个关联表来存储记录间的关联,该表仅有两个 ID 字段,为两张关联表的外键。默认关联表名由两个表名中间加下划线并在最后加上_rel 来组成。我们图书和作者关联,表名应为library_book_res_partner_rel。

有时我们可能需要重写这种自动生成的默认值。一种情况是关联模型名称过长,导致关联表名的长度超出PostgreSQL数据库63个字符的上限。这时就需要手动选择一个关联表名来符合字符数据要求。另一种情况是我们需要在相同模型间建立第二张many-to-many关联表。这时也需要手动提供一个关联表名来避免与已存在的第一张表名冲突。

有两种方案来重写关联表名:位置参数或关键字参数。通过字段位置参数定义示例如下:

1
2
3
4
5
6
7
# Book <-> Authors关联(使用位置参数)
author_ids = fields.Many2many(
'res.partner', # 关联模型(尾款)
'library_book_res_partner_rel', # 要使用的关联表名
'a_id', # 本记录关联表字段
'p_id', # 关联记录关联表字段
'Authors') # string标签文本

要使可读性更强,也可使用关键字参数:

1
2
3
4
5
6
7
# Book <-> Authors关联(使用关键字参数)
author_ids = fields.Many2many(
comodel_name='res.partner', # 关联模型(必填)
relation='library_book_res_partner_rel', # 关联表名
column1='a_id', # 本记录关联表字段
column2='p_id', # 关联记录关联表字段
string='Authors') # string标签文本

与one-to-many relational字段相似,many-to-many 字段还可以使用context, domain和auto_join这些关键字参数。

ℹ️在创建抽象模型时,many-to-many中不要使用column1和column2属性。在 ORM 设计中对抽象模型有一个限制,如果指定关联表列名,就无法再被正常继承。

层级关联

父子树状关联使用同一模型中many-to-one关联表示,来将每条记录引用其父级。反向的one-to-many关联对应记录的子级。Odoo 通过域表达式附加的child_of和parent_of操作符改良了对这些层级数据结构的支持。只要这些模型有parent_id字段(或_parent_name有效模型定义)就可以使用这些操作符。

通过设置_parent_store=True和添加parent_path帮助字段可加快层级树的查询速度。该字段存储用于加速查询速度的层级树结构信息。

ℹ️Odoo 12中的修改
parent_path帮助字段在 Odoo 12中引入。此前版本中使用parent_left和parent_right整型字段来实现相同功能,但在 Odoo 12中淘汰了这些字段。

注意这些附加操作会带来存储和执行速度的开销,所以最好是用到读的频率大于写的情况下,比如本例中的分类树。仅在优化多节点深度层级时才需要使用,对于小层级或浅层级的可能会被误用。

为演示层级结构,我们将为图书应用添加一个分类树,用于为图书分类。在library_app/models/library_book_category.py文件中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from odoo import api, fields, models

class BookCategory(models.Model):
_name = 'library.book.category'
_description = 'Book Category'
_parent_store = True

name = fields.Char(translate=True, required=True)
# Hierarchy fields
parent_id = fields.Many2one(
'library.book.category',
'Parent Category',
ondelete='restrict')
parent_path = fields.Char(index=True)

# Optional but good to have:
child_ids = fields.One2many(
'library.book.category',
'parent_id',
'Subcategories')

这里定义了一个基本模型,包含引用父级记录的parent_id字段。为启用层级索引来加快树级搜索,添加了一个_parent_store=True 模型属性。使用该属性必须要添加且必须要索引parent_path字段。引用父级的字段名应为parent_id,但如果声明了可选的_parent_name模型属性,则可以使用任意其它字段名。

添加字段列出直接的子级非常方便,即为上述代码中的one-to-many反向关联。还有不要忘记在library_app/models/init.py文件中添加对以上代码的引用:

1
2
3
from . import library_book
from . import res_partner
from . import library_book_category

使用引用字段的弹性关联

普通关联字段指定固定的引用co-model模型,但Reference字段类型不受这一限制,它支持弹性关联,因此相同字段不用限制只指向相同的目标模型。作为示例,我们使用图书分类模型来添加引用重点图书或作者。因此该字段可引用图书或 partner:

1
2
3
4
5
6
class BookCategory(models.Model):
...
highlighted_id = fields.Reference(
[('library.book', 'Book'), ('res.partner', 'Author')],
'Category Highlight'
)

该字段定义与 selection 字段相似,但这里选择项为该字段中可以使用的模型。在用户界面中,用户会先选择列表中的模型,然后选择模型中的指定记录。

ℹ️Odoo 12中的修改
删除了可引用模型配置表。在此前版本中,可用于配置在 Reference 字段中可用的模型。通过菜单Settings > Technical > Database Structure可进行查看。这些配置可在 Reference 字段中使用odoo.addons.res.res_request.referenceable_models函数来替代模型选择列表。

以下为有关引用字段的一些其它有用技术细节:

  • 引用字段在数据库中以model,id字符串形式存储
  • read()方法供外部应用使用,以格式化的(‘model_name’, id)元组返回,而不是常用的many-to-one字段的(id, ‘display_name’)形式

计算字段

字段值除普通的读取数据库中存储值外,还可自动由函数计算。计算字段的声明和普通字段相似,但有一个额外的compute参数来定义用于计算的函数。大多数情况下,计算字段包含书写业务逻辑。因此要完全使用这一功能,还要学习第八章 Odoo 12开发之业务逻辑 - 业务流程的支持。此处我们将解释计算字段用法,但会使用简单的业务逻辑。

图书有出版商,我们的例子是在图书表单中添加出版商的国别。实现该功能,我们会使用基于publisher_id的计算字段,将会从出版商的country_id字段中获取值。

编辑library_app/models/library_book.py文件中的图书模型,代码如下:

1
2
3
4
5
6
7
8
9
10
11
class Book(models.Model):
...
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country'
)

@api.depends('publisher_id.country_id')
def _compute_publisher_country(self):
for book in self:
book.publisher_country_id = book.publisher_id.country_id

以上代码添加了一个publisher_country_id字段,和一个计算其值的_compute_publisher_country方法。方法名作为字符串参数传入字段中,但也可以传递一个可调用引用(方法标识符,不带引号)。但这时需确定Python 文件中方法在字段之前定义。

计算如果依赖其它字段的话就需要使用@api.depends装饰器,通常都会依赖其它字段。它告诉服务器何时重新计算或缓存值。参数可接受一个或多个字段名,点号标记可用于了解字段关联。本例中,只要图书publisher_id的country_id变更了就会重新进行计算。

和平常一样,self 参数是要操作的字符集对象。我们需要对其遍历来作用于每条记录。计算值通过常用(写)操作来设置,本例中计算相当简单,我们为其分配当前图书的publisher_id.country_id值。

同样的计算方法可用于一个以上字段。这时同一方法在多个compute 字段参数中使用,计算方法将为所有计算字段分配值。

小贴士: 计算函数必须为一个或多个字段分配值用于计算。如果计算方法有 if 条件分支,确保每个分支中为计算字段分配了值。否则在未分配置值的分支中将会报错。

现在我们还不会修改该模块的视图,但可通过在图书表单视图中点击开发者菜单中的Edit View选项,直接在表单 XML 中添加该字段查看效果。不必担心出问题,在下次模块升级时会进行覆盖。

Odoo 12图书项目出版商国家

搜索和写入计算字段

我们刚刚创建的计算字段可读取但不可搜索或写入。默认情况下计算字段是实时计算,而不存储在数据库中。这也是无法像普通字段那样进行搜索的原因。

我们可通过实现特殊方法来开启搜索和写入操作。计算字段可与 compute 方法一起设置实现搜索逻辑的 search 方法,以及实现写入逻辑的 inverse 方法。使用这些方法,计算字段可修改如下:

1
2
3
4
5
6
7
8
9
class Book(models.Model):
...
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
compute='_compute_publisher_country',
# store = False, # 默认不在数据库中存储
inverse='_inverse_publisher_country',
search='_search_publisher_country',
)

计算字段中的写入是计算的反向(inverse)逻辑。因此处理写入操作的方法称为 inverse,本例中 inverse 方法很简单。计算将book.publisher_id.country_id 的值复制给book.publisher_country_id,反向操作是将写入book.publisher_country_id的值拷贝给book.publisher_id.country_id field字段:

1
2
3
def _inverse_publisher_country(self):
for book in self:
book.publisher_id.country_id = book.publisher_country_id

注意这会修改出版商partner记录数据,因此也会修改相同出版商图书的相关字段。常规权限控制对这类写操作有效,因此仅有对 partner 模型有写权限的当前用户才能成功执行操作。

要为计算字段开启搜索操作,需要实现search 方法。为此我们需要能够将计算字段的搜索转换为使用常规存储字段的搜索域。本例中,实际的搜索可通过关联的publisher_id Partner 记录的country_id来实现:

1
2
def _search_publisher_country(self, opearator, value):
return [('publisher_id.country_id', operator, value)]

在模型上执行搜索时,域表达式用作实施过滤的参数。域表达式在第八章 Odoo 12开发之业务逻辑 - 业务流程的支持会做详细讲解,现在我们应了解它是一系列(field, operator, value)条件。

当域表达式的条件中出现该计算字段时就会调用这个搜索方法。它接收搜索的操作符和值,并将原搜索元素转换为一个域搜索表达式。country_id字段存储在关联的partner模型中,因此我们的搜索实现仅需修改原搜索表达式来使用publisher_id.country_id字段。

存储计算字段

通过在定义时设置store = True还可以将计算字段值保存到数据库中。在任意依赖变更时值就会重新计算。因为值已被存储,所以可以像普通字段一样被搜索,也就不需要使用 search 方法了。

关联字段

前面我们实现的计算字段仅仅是从关联记录中将值拷贝到模型自己的字段中。这种常用情况可以由 Odoo 使用关联字段功能自动处理。关联字段通过关联模型的字段可在模型中直接可用,并且可通过点号标记法直接访问。这样在点号标记法不可用时(如 UI 表单视图)也可以使用该字段。

要创建关联字段,我们像普通计算字段那样声明一个所需类型的字段,但使用的不是 compute 属性,而是 related属性,设置用点号标记链来使用所需字段。我们可以使用引用字段来获取与上例publisher_country_id计算字段相同的效果:

1
2
3
4
publisher_country_id = fields.Many2one(
'res.country', string='Publisher Country',
related='publisher_id.country_id',
)

本质上关联字段仅仅是快捷实现 search 和 inverse 方法的计算字段。也就是说可以直接对其进行搜索和写入,而无需书写额外的代码。默认关联字段是只读的,因inverse写操作不可用,可通过readonly=False字段属性来开启写操作。

ℹ️Odoo 12中的修改
现在关联字段默认为只读:readonly=True。此前版本中它默认可写,但事实证明这是一个默认值,因为它可能会允许修改配置或主数据这些不应被修改的数据。

还应指出这些关联字段和计算字段一样可使用store=True来在数据库中存储。

模型约束

通常应用需保证数据完整性,并执行一些验证来保证数据是完整和正确的。PostgreSQL数据库管理器支持很多可用验证:如避免重复,或检查值以符合某些简单条件。模型为此可声明并使用 PostgreSQL约束。一些检查要求更复杂的逻辑,最好是使用 Python 代码来实现。对这些情况,我们可使用特定的模型方法来实现 Python 约束逻辑。

SQL模型约束

SQL约束加在数据表定义中,并由PostgreSQL直接执行。它由_sql_constraints类属性来定义。这是一个元组组成的列表,并且每个元组的格式为(name, code, error):

  • name是约束标识名
  • code是约束的PostgreSQL语法
  • error是在约束验证未通过时向用户显示的错误消息

我们将向图书模型添加两个SQL约束。一条是唯一性约束,用于通过标题和出版日期是否相同来确保没有重复的图书;另一条是检查出版日期是否为未出版:

1
2
3
4
5
6
7
8
9
10
class Book(models.Model):
...
_sql_constraints = [
('library_book_name_date_uq', # 约束唯一标识符
'UNIQUE (name, date_published)', # 约束 SQL 语法
'Book title and publication date must be unique'), # 消息
('library_book_check_date',
'CHECK (date_published <= current_date)',
'Publication date must not be in the future.'),
]

更多有关PostgreSQL约束语法,请参见官方文档

Python模型约束

Python 约束可使用自定义代码来检查条件。检查方法应添加@api.constrains装饰器,并且包含要检查的字段列表,其中任意字段被修改就会触发验证,并且在未满足条件时抛出异常。就图书应用来说,一个明显的示例就是防止插入不正确的 ISBN 号。我们已经在_check_isbn()方法中书写了 ISBN 的校验逻辑。可以在模型约束中使用它来防止保存错误数据:

1
2
3
4
5
6
7
8
9
from odoo.exceptions import ValidationError

class Book(models.Model):
...
@api.constrains('isbn')
def _constrain_isbn_valid(self):
for book in self:
if book.isbn and not book._check_isbn():
raise ValidationError('%s is an invalid ISBN' % book.isbn)

了解 Odoo的 base 模型

在前面文章中,我们一起创建了新模型,如图书模型,但也使用了已有的模型,如  Odoo 自带的Partner 模型。下面就来介绍下这些内置模型。Odoo 内核中有一个base插件模块。它提供了 Odoo 应用所需的基本功能。然后有一组内置插件模块来提供标准产品中的官方应用和功能。base模块中包含两类模型:

  • 信息仓库(Information Repository), ir.*模型
  • 资源(Resources), res.*模型

信息仓库用于存储 Odoo 所需数据,以知道如何作为应用来运作,如菜单、视图、模型、Action 等等。Technical菜单下的数据通常都存储在信息仓库中。相关的例子有:

  • ir.actions.act_window用于窗口操作
  • ir.ui.menu用于菜单项
  • ir.ui.view用于视图
  • ir.model用于模型
  • ir.model.fields用于模型字段
  • ir.model.data用于XML ID

资源包含基本数据,基本上用于应用。以下是一些重要的资源模型:

  • res.partner用于业务伙伴,如客户、供应商和地址等等
  • res.company用于公司数据
  • res.currency用于货币
  • res.country用于国家
  • res.users用于应用用户
  • res.groups用于应用安全组

这些应该有助于你在未来遇到这些模型时理解它们来自何处。

总结

学习完本文,我们熟悉了模型带给我们构造数据模型的可能性。我们看到模型通常继承models.Model类,但还可使用models.Abstract来创建可复用的 mixin 模型、使用models.Transient来创建向导或高级用户对话。我们还学习了常见的模型属性,如_order 用于排序,_rec_name用于记录展示的默认值。

模型中的字段定义了所有它存储的数据。我们了解了可用的非关联字段类型以及它们支持的属性。我们还学习了关联字段的几种类型:many-to-one, one-to-many和many-to-many,以及它们如何定义模型间的关系,包括层级父子关系。

大多数字段在数据库中存储用户的输入,但字段也可以通过 Python 代码自动计算值。我们看到了如何实现计算字段,以及一些高级用法,如使计算字段可写及可搜索。

还有模型定义的一部分是约束,保持数据一致性和执行验证,可以通过PostgreSQL或Python代码实现。

一旦我们创建了数据模型,就应该为它提供一些默认和演示数据。在下一篇文章中我们将学习如何使用数据文件在系统中导入、导出和加载数据。

 

☞☞☞第七章 Odoo 12开发之记录集 - 使用模型数据

本文首发地址:Alan Hou 的个人博客