本文为最好用的免费ERP系统Odoo 12开发手册系列文章第九篇。
Odoo 服务器端带有外部 API,可供网页客户端和其它客户端应用使用。本文中我们将学习如何在我们的客户端程序中使用 Odoo 的外部 API。为避免引入大家所不熟悉的编程语言,此处我们将使用基于 Python 的客户端,但这种 RPC 调用的处理方法也适用于其它编程语言。
我们将一起了解如何使用 Odoo RPC调用,然后根据所学知识使用 Python创建一个简单的图书命令行应用。
本文主要内容有:
- 在客户端机器上安装 Python
- 使用XML-RPC连接 Odoo
- 使用XML-RPC运行服务器端方法
- 搜索和读取 API 方法
- 图书客户端XML-RPC 接口
- 图书客户端用户界面
- 使用OdooRPC库
- 了解ERPpeek客户端
开发准备
本文基于第三章 Odoo 12 开发之创建第一个 Odoo 应用创建的代码,具体代码请参见 GitHub 仓库。应将library_app模块放在addons路径下并进行安装。为保持前后一致,我们将使用第二章 Odoo 12开发之开发环境准备所进行安装并使用12-library数据库。本章完成后的代码请参见 GitHub 仓库。
补充:因原书前后曾使用过多个数据库,本文中Alan将使用系列中一直使用的 dev12数据库,并在前一篇文章的基础上进行开发。
学习项目-图书目录客户端
本文中,我们将开发一个简单的客户端应用来管理图书目录。这是一个命令行接口(CLI) 应用,使用 Odoo 来作为后端。应用的功能非常有限,这样我们可以聚焦在用于与 Odoo服务端交互的技术,而不是具体应用的实现细节。我们的简单应用可以完成如下功能:
- 通过标题搜索并列出图书
- 向目录添加新标题
- 修正图书标题
- 从目录中删除图书
这个应用是一个 Python 脚本,等待输入命令来执行操作。使用会话示例如下:
1 2 3 4 5 | $ python3 library.py add "Moby-Dick" $ python3 library.py list "moby" 60 Moby-Dick $ python3 library.py set-title 60 "Moby Dick" $ python3 library.py del 60 |
在客户端机器上安装 Python
Odoo API 可以在外部通过两种协议访问:XML-RPC和JSON-RPC。任意外部程序,只要能实施其中一种协议的客户端,就可以与 Odoo 服务端进行交互。为避免引入其它编程语言,我们将保持使用 Python 来研究外部 API。
到目前为止我们仅在服务端运行了 Python 代码。现在我们要在客户端上使用 Python,所以你可能需要在电脑上做一些额外设置。要学习本文的示例,你需要能在操作电脑上运行 Python 文件。可通过在命令行终端运行python3 –version命令来进行确认。如果没有安装,请参考官方网站针对你所使用的平台的安装包。
对于 Ubuntu,你可能已经安装了 Python 3,如果没有安装,可通过以下命令进行安装:
1 | sudo apt-get install python3 python3-pip |
如果你使用的是 Windows 并且已安装了 Odoo,可能会奇怪为什么没有 Python 解释器,还需要进行额外安装。简单地说是因为 Odoo 安装带有一个内嵌的 Python 解释器,无法在外部使用。
使用XML-RPC连接 Odoo API
访问服务的最简单方法是使用XML-RPC,我们可以使用 Python 标准库中的xmlrpclib来实现。不要忘记我们是在编写客户端程序连接服务端,因此需运行 Odoo 服务端实例来供连接。本例中我们假设 Odoo 服务端实例在同一台机器上运行,但你可以使用任意运行服务的其它机器,只要能连接其IP 地址或服务器名。
让我们来初次连接 Odoo 外部 API。打开 Python 3终端并输入如下代码:
1 2 3 4 5 | >>> from xmlrpc import client >>> srv = 'http://localhost:8069' >>> common = client.ServerProxy('%s/xmlrpc/2/common' % srv) >>> common.version() {'server_version': '12.0', 'server_version_info': [12, 0, 0, 'final', 0, ''], 'server_serie': '12.0', 'protocol_version': 1} |
这里我们导入了xmlrpc.client库,然后创建了一个包含服务地址和监听端口信息的变量。请根据自身状况进行修改(如 Alan 使用srv = ‘http://192.168.16.161:8069’)。
下一步访问服务端公共服务(无需登录),在终端地址/xmlrpc/2/common上暴露。其中一个可用方法是version(),用于查看服务端版本。我们使用它来确认可与服务端进行通讯。
另一个公共方法是authenticate()。你可能你会以为它会创建会话,但实际上不会。该方法仅仅确认用户名和密码可被接受,请求不使用用户名而是它返回的用户 ID。示例如下:
1 2 3 4 5 | >>> db = 'dev12' >>> user, pwd = 'admin', 'admin' >>> uid = common.authenticate(db, user, pwd, {}) >>> print(uid) 2 |
首先创建变量 db,来存储使用的数据库名。本例中为 dev12,但可以修改为任意其它你所使用的数据库名。如果登录信息错误,将不会返回用户 ID,而是返回 False 值。authenticate()最后一个参数是用户代理(User Agent)环境,用于提供客户端的一些元数据(metadata),它是必填的,但可以是一个空字典。
![](https://i-blog.csdnimg.cn/blog_migrate/60ac2479baab04c4fca87f732d6bd625.jpeg)
使用XML-RPC运行服务器端方法
使用XML-RPC,不会维护任何会话,每次请求都会发送验证信息。这让协议过重,但使用简单。下一步我们设置访问需登录才能访问的服务端方法。暴露的终端地址为/xmlrpc/2/object,示例如下:
1 2 3 | >>> api = client.ServerProxy('%s/xmlrpc/2/object' % srv) >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search_count', [[]]) 48 |
此处我们第一次访问了服务端 API,执行了Partner 记录的计数。通过 execute_kw() 方法来调用方法,接收如下参数:
- 连接的数据库名
- 连接用户ID
- 用户密码
- 目标模型标识符名称
- 调用的方法
- 位置参数列表
- 可选的关键字参数字典(本例中未使用)
上面的例子中对res.partner模型调用了search_count方法,仅一个位置参数[],没有关键字参数。该位置参数是一个搜索域,因我们传入的是一个空列表,它对所有伙伴进行计数。常用的操作有搜索和读取。在使用RPC调用时,search方法返回一个区块的 ID 列表。browse方法不可用于RPC,而应使用read来得到记录 ID 列表并获取相应数据,示例如下:
1 2 3 4 5 | >>> domain = [('is_company', '=', True)] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'search', [domain]) [14, 10, 11, 15, 12, 13, 9, 1] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[14]], {'fields': ['id', 'name', 'country_id']}) [{'id': 14, 'name': 'Azure Interior', 'country_id': [233, 'United States']}] |
对于 read 方法,我们使用了一个位置参数[14]来作为 ID 列表,以及一个关键字参数fields。还可以看到many-to-one关联字段如country_id,被成对获取,包含关联的记录 ID 和显示名称。在代码中处理数据时应记住这一点。
经常会使用search和 read 的组合,所以提供了一个search_read方法来在同一步中执行两者的操作。可通过如下命令来获取以上两段代码的同样结果:
1 | api.execute_kw(db, uid, pwd, 'res.partner', 'search_read', [domain], {'fields': ['id', 'name', 'country_id']}) |
补充:以上代码会为 read 方法传入所有 search 方法的结果,因此内容要较仅传入[14]多
search_read方法和 read 相似,但需要 domain代替 id 列表来作为第一个位置参数。需要说明在 read 和search_read中fields参数并非必须。如果不传入,则获取所有字段。这可能会带来对函数字段的大量计算,并且获取大量可能从来都不会用到的数据,所以通常建议明确传入字段列表。
搜索和读取 API 方法
在第七章 Odoo 12开发之记录集 – 使用模型数据我们学习了用于生成记录的最重要的模型方法以及代码书写。但还有一些其它模型方法可用于更具体的操作,如:
- read([fields]) 类似于browse方法,但返回的不是记录集,而是包含按参数指定的字段的各行数据列表。每一行是一个字典。它提供可供 RPC 协议使用的数据的序列化展示,设计用于客户端程序中而非服务端逻辑中。
- search_read([domain], [fields], offset=0, limit=None, order=None)在读取结果记录列表之后执行搜索操作。设计用于 RPC 客户端,避免了反复进行读取结果和搜索的操作。
所有其它模型方法都对 RPC 暴露,但以下划线开头的除外,这些是私有方法。也就是说我们可以像下面这样使用create, write,和unlink修改服务端数据:
1 2 3 4 5 6 7 8 9 10 11 | >>> x = api.execute_kw(db, uid, pwd, 'res.partner', 'create', [{'name': 'Packt Pub'}]) >>> print(x) 69 >>> api.execute_kw(db, uid, pwd, 'res.partner', 'write', [[x], {'name': 'Packt Publishing'}]) True >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[x], ['id', 'name']]) [{'id': 69, 'name': 'Packt Publishing'}] >>> api.execute_kw(db, uid, pwd, 'res.partner', 'unlink', [[x]]) True >>> api.execute_kw(db, uid, pwd, 'res.partner', 'read', [[x], ['id', 'name']]) [] |
XML-RPC的一个缺陷是它不支持 None 值。有一个XML-RPC扩展可以支持 None 值,但这取决于我们客户端所依赖的具体XML-RPC库。不返回任何值的方法不能在XML-RPC中使用,因为默认返回的是 None。这也是为什么方法在结束时至少会带有一个return True语句。另一个方案是使用 Odoo 同时支持的JSON-RPC。OdooRPC对其进行运行,在稍后的使用OdooRPC库一节会进行使用。
应反复强调 Odoo 的外部 API 可在大部分编程语言中使用。官方文档中我们可以看到Ruby, PHP和Java实际示例。
ℹ️以下划线开头的模块方法被认为是私有方法,不对XML-RPC暴露。
图书客户端XML-RPC 接口
下面就来实现图书客户端应用。我们将使用两个文件:一个处理服务端的接口:library_api.py,另一个处理应用的用户界面:library.py。然后我们会使用现有的OdooRPC库来提供一个替代的实现方法。
我们将创建类来配置与 Odoo 服务端的连接,以及读取/写入图书数据。这将暴露基本的增删改查方法:
- search_read()获取图书数据
- create()创建图书
- write()更新图书
- unlink()删除图书
选择一个目录来放置应用文件并创建library_api.py文件。首先添加类的构造方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | from xmlrpc import client class LibraryAPI(): def __init__(self, srv, port, db, user, pwd): common = client.ServerProxy( 'http://%s:%d/xmlrpc/2/common' % (srv, port)) self.api = client.ServerProxy( 'http://%s:%d/xmlrpc/2/object' % (srv, port)) self.uid = common.authenticate(db, user, pwd, {}) self.pwd = pwd self.db = db self.model = 'library.book' |
此处我们存储了所有创建执行模型调用的对象的所有信息:API引用、uid、密码、数据库名和要使用的模型。接下来我们定义一个帮助方法来执行调用。有赖于前面对象存储的数据该方法可以很精炼:
1 2 3 4 | def execute(self, method, arg_list, kwarg_dict=None): return self.api.execute_kw( self.db, self.uid, self.pwd, self.model, method, arg_list, kwarg_dict or {}) |
现在就可以使用它来实现更高级的方法了。search_read()方法接收一个可选的 ID 列表来获取数据。如果没传入数据,则返回所有记录:
1 2 3 4 | def search_read(self, text=None): domain = [('name', 'ilike', text)] if text else [] fields = ['id', 'name'] return self.execute('search_read', [domain, fields]) |
create()方法用于创建给定书名的新书并返回所创建记录的 ID:
1 2 3 | def create(self, title): vals = {'name': title} return self.execute('create', [vals]) |
write()方法中传入新书名和图书 ID 作为参数,并对该书执行写操作:
1 2 3 | def write(self, title, id): vals = {'name': title} return self.execute('write', [[id], vals]) |
然后我们可以实现unlink()方法,非常简单:
1 2 | def unlink(self, id): return self.execute('unlink', [[id]]) |
在该Python文件最后添加一段测试代码在运行时执行:
1 2 3 4 5 6 7 | if __name__ == '__main__': # 测试配置 srv, db, port = 'localhost', 'dev12', 8069 user, pwd = 'admin', 'admin' api = LibraryAPI(srv, port, db, user, pwd) from pprint import pprint pprint(api.search_read()) |
如果执行以上 Python 脚本,我们可以打印出图书的内容:
1 2 3 4 5 6 7 8 | $ python3 library_api.py [{'id': 56, 'name': 'Brave New World'}, {'id': 40, 'name': 'Hands-On System Programming with Linux'}, {'id': 41, 'name': 'Lord of the Flies'}, {'id': 39, 'name': 'Mastering Docker - Third Edition'}, {'id': 38, 'name': 'Mastering Reverse Engineering'}, {'id': 55, 'name': 'Odoo 11 Development Cookbook'}, {'id': 54, 'name': 'Odoo Development Essentials 11'}] |
现在已经有了对 Odoo 后端的简单封装,下面就可以处理命令行用户界面了。
图书客户端用户界面
我的目标是学习如何写外部应用和 Odoo 服务之间的接口,前面已经实现了。但不能止步于此,我们还应让终端用户可以使用它。为使设置尽量简单,我们将使用 Python 内置功能来实现这个命令行应用。该功能是标准库自带的,因此不需要进行额外的安装。
在library_api.py 同目录,新建一个library.py文件。首先导入命令行参数解析器,然后导入LibraryAPI类,代码如下:
1 2 | from argparse import ArgumentParser from library_api import LibraryAPI |
下面我们来看看参数解析器接收的命令,有以下四个命令:
- 搜索并列出图书
- 添加图书
- 设置(修改)书名
- 删除图书
在命令行解析器中添加这些命令的代码如下:
1 2 3 4 5 6 | parser = ArgumentParser() parser.add_argument( 'command', choices=['list', 'add', 'set-title', 'del']) parser.add_argument('params', nargs='*') # 可选参数 args = parser.parse_args() |
这里 args 是一个包含传入脚本参数的对象,args.command是提供的命令,args.params是可选项,用于存放命令所需的其它参数。如果使用了不存在或错误的命令,参数解析器会进行处理并提示用户应输入的内容。有关argparse更完整的说明,请参考官方文档。
下一步是执行所计划的操作。首先为 Odoo服务准备连接:
1 2 3 | srv, port, db = 'localhost', 8069, 'dev12' user, pwd = 'admin', 'admin' api = LibraryAPI(srv, port, db, user, pwd) |
第一行代码设置服务实例的一些固定参数以及要连接的数据库。本例中,我们连接 Odoo 服务本机(localhost),监听8069默认端口,并使用 dev12数据库。如需连接其它服务器和数据库,请对参数进行相应调整。
这里硬编码了服务器地址并且密码使用了明文,显然与最佳实施相差甚远。我们应该包含配置步骤让客户提供相关设置信息,并以安全的方式进行存储。但此处我们的目标是学习使用 Odoo RPC,所以可把它看作概念代码,而非已完成的产品。下面写代码来利用 api 对象处理所支持的命令。我们可以先写list命令来列出图书:
1 2 3 4 5 | if args.command == 'list': text = args.params[0] if args.params else None books = api.search_read(text) for book in books: print('%(id)d %(name)s' % book) |
这里我们使用了LibraryAPI.search_read()来从服务端获取图书记录列表。然后遍历列表中每个元素并打印。我们使用 Python 字符串格式化来向用户显示每条图书记录,记录是一个键值对字典。下面添加add命令,这里使用了额外的书名作为参数:
1 2 3 4 | if args.command == 'add': for title in args.params: new_id = api.create(title) print('Book added with ID %d.' % new_id) |
因为主要的工作已经在LibraryAPI对象中完成,下面我们只要调用write()方法并向终端用户显示结果即可。 set-title命令允许我们修改已有图书的书名,应传入两个参数,新的书名和图书的 ID:
1 2 3 4 5 6 7 | if args.command == 'set-title': if len(args.params) != 2: print("set command requires a title and ID.") else: book_id, title = int(args.params[0]), args.params[1] api.write(title, book_id) print('Title set for Book ID %d.' % book_id) |
最终我们要实现 del 命令来删除图书记录,学到这里应该不再有任何挑战性了:
1 2 3 4 | if args.command == 'del': for param in args.params: api.unlink(int(param)) print('Book with ID %s deleted.' % param) |
到这里我们就完成了基础的 API CLI (命令行接口)了,读者可以尝试执行命令来查看效果。比如,我们可以运行本文开头学习项目-图书目录客户端中的命令。通过普通客户端来访问图书中的数据也会有助于确认该CLI是否如预想般运行。这是一个非常基础的应用,查看代码你应该可以想到一些改进它的方式。但要记住我们这里的目的是以相对有趣的方式举例说明Odoo RPC API的使用。
![](https://i-blog.csdnimg.cn/blog_migrate/3d5b5a4cc6d65289d125ecf67dd07ce9.jpeg)
使用OdooRPC库
另一个可以考虑的客户端库是OdooRPC。它是一个更流行的客户端库,使用JSON-RPC 协议而不是XML-RPC。事实上 Odoo 官方客户端使用的就是JSON-RPC,XML-RPC更多是用于支持向后兼容性。
ℹ️OdooRPC库现在由 OCA 管理和持续维护。了解更多请参见OCA。
OdooRPC库可通过PyPI安装:
1 | pip3 install --user odoorpc |
不管是使用JSON-RPC还是XML-RPC,Odoo API的使用方式并没什么分别。所以在下面我们可以看一些细节可能有区别,但这些客户端库的使用方式并没有什么分别。
OdooRPC库在创建新的odoorpc.ODOO对象时建立服务端连接,然后应使用ODOO.login()方法来创建用户会话。和服务端相似,会话有一个带有会话环境的 env 属性,包含用户 ID-uid 和上下文。我们可以使用OdooRPC来重新实现library_api.py对服务端的接口。它应提供相同的功能,但使用JSON-RPC代替XML-RPC来实施。在相同目录下创建library_odoorpc.py文件并加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from odoorpc import ODOO class LibraryAPI(): def __init__(self, srv, port, db, user, pwd): self.api = ODOO(srv, port=port) self.api.login(db, user, pwd) self.uid = self.api.env.uid self.model = 'library.book' self.Model = self.api.env[self.model] def execute(self, method, arg_list, kwarg_dict=None): return self.api.execute( self.model, method, *arg_list, **kwarg_dict) |
OdooRPC库实现Model和Recordset对象来模仿服务端对应的功能。目标是在客户端编程与服务端编程应基本一致。客户端使用的方法将通过存储在self.Model属性中的图书模型来利用这点。这里实现的execute()方法并不会在我们客户端中使用,仅用于与本文中讨论的其它实现进行对比。
下面我们来实现search_read(), create(), write()和unlink()这些客户端方法。在相同文件的LibraryAPI()类中添加如下方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def search_read(self, text=None): domain = [('name', 'ilike', text)] if text else [] fields = ['id', 'name'] return self.Model.search_read(domain, fields) def create(self, title): vals = {'name': title} return self.Model.create(vals) def write(self, title, id): vals = {'name': title} self.Model.write(id, vals) def unlink(self, id): return self.Model.unlink(id) |
注意这段代码和 Odoo 服务端代码相似,因为它使用了与 Odoo 中插件写法相近的 API。然后可以将library.py文件中的from library_api import LibraryAPI一行修改为library_odoorpc import LibraryAPI。现在再次运行library.py客户端应用进行测试,执行的效果和之前应该一致。
了解ERPpeek客户端
ERPpeek是一个多功能工具,既可以作为交互式命令行接口(CLI)也可以作为 Python库,它提供了比xmlrpc库更便捷的 API。它在PyPi索引中,可通过如下命令安装:
1 | pip3 install --user erppeek |
ERPpeek不仅可用作 Python 库,它还可作为 CLI 来在服务器上执行管理操作。Odoo shell 命令在主机上提供了一个本地交互式会话功能,而erppeek库则为网络上的客户端提供了一个远程交互式会话。打开命令行,通过以下命令可查看能够使用的选项:
1 | erppeek --help |
下面一起来看看一个示例会话:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $ erppeek --server='http://192.168.16.161:8069' -d dev12 -uadmin Usage (some commands): models(name) # List models matching pattern model(name) # Return a Model instance ... Password for 'admin': Logged in as 'admin' dev12 >>> model('res.users').count() 3 dev12 >>> rec = model('res.partner').browse(14) dev12 >>> rec.name 'Azure Interior' |
如上所见,建立了服务端的连接,执行上下文引用了model() 方法来获得模型实例并对其进行操作。连接使用的erppeek.Client实例也可通过客户端变量来使用。 值得一提的是它可替代网页客户端来管理所安装的插件模块:
- client.modules()列出可用或已安装模块
- client.install()执行模块安装
- client.upgrade()执行模块升级
- client.uninstall()卸载模块
因此ERPpeek可作为 Odoo 服务端远程管理的很好的服务。有关ERPpeek的更多细节请见 GitHub。
![](https://i-blog.csdnimg.cn/blog_migrate/c96e44ac1382a80bde6f73cc644b97c2.jpeg)
总结
本文的目标是学习外部 API 如何运作以及它们能做些什么。一开始我们通过一个简单的Python XML-RPC客户端来进行探讨,但外部 API 可用于其它编程语言。事实上官方文档中包含了Java, PHP和Ruby的代码示例。
有很多库可处理XML-RPC或JSON-RPC,有些是通用的,有些仅适用于 Odoo。我们使用了一个指定库OdooRPC。
以上我们就完结了本文有关编程 API 和业务逻辑的学习。是时候深入视图和用户界面了。在下一篇文章中,我们进一步学习网页客户端所提供的后台视图和用户体验。
☞☞☞第十章 Odoo 12开发之后台视图 – 设计用户界面