Odoo 服务端提供有外部 API,可供网页客户端和其它客户端应用使用。本章中我们将学习如何Odoo 的外部 API来实现将其Odoo服务端作为后端的外部应用。
可通过编写脚本来加载或修改Odoo数据,或是集成Odoo现有的业务应用,作为对Odoo应用一种补充。
我们将描述如何使用Odoo RPC调用,然后根据所学知识使用 Python为图书应用创建一个简单的命令行应用。
本章的主要内容有:
- 介绍学习项目:图书目录的客户端应用
- 在客户端机器上配置 Python
- 探索Odoo的外部API
- 实现客户端应用的XML-RPC接口
- 实现客户端应用的用户界面
- 使用OdooRPC库
学完本章后,读者可以创建一个简单的Python应用,使用Odoo作为后端进行查询和数据存储。
开发准备
本文基于第三章 Odoo 15开发之创建第一个 Odoo 应用创建的代码,具体代码请参见 GitHub 仓库。应将library_app模块放在addons路径下并进行安装。为保持前后一致,我们将使用第二章 Odoo 15开发之开发环境准备中的安装操作。本章完成后的代码请参见 GitHub 仓库。
学习项目-图书目录客户端
本文中,我们将开发一个简单的客户端应用来管理图书目录。这是一个命令行接口(CLI) 应用,使用 Odoo 来作为后端。应用的功能很基础,核心放在用于与 Odoo服务端交互的技术。
这个简单CLI应用可以完成如下功能:
- 通过标题搜索并列出图书
- 向目录添加新书籍
- 编辑图书标题
我们的主要目标是使用Odoo对外API,因此不希望引用其它读者可能不太熟悉的编程语言。有了这一出发点,最好的方式是就是使用Python来实现客户端应用。不过只要掌握了一种语言的XML-RPC库,相关处理RPC的技术同样适用于其它语言。
这个应用是一个 Python 脚本,等待输入命令来执行操作。示例如下:
1 2 3 4 |
$ python3 library.py add "Moby-Dick" $ python3 library.py list "moby" 60 Moby-Dick $ python3 library.py set 60 "Moby Dick" |
这个示例会话演示了如何使用客户端应用添加、列出及修改图书标题。
该客户端应用通过Python运行,在开始编写客户端应用代码之前,应确保在客户端机器上安装有Python。
在客户端机器上安装 Python
Odoo API 可以在外部通过两种协议访问:XML-RPC和JSON-RPC。任何外部程序,只要是能实现其中一种协议的客户端,就可以与 Odoo 服务端进行交互。为避免引入其它编程语言,我们将保持使用 Python 来探讨外部 API。
到目前为止我们仅在服务端运行了 Python 代码。现在我们要在客户端上使用 Python,所以你可能需要在电脑上做一些额外设置。
要学习本文的示例,你需要能在操作电脑上运行 Python 3 代码。如果在前面已按前面章节配置过开发环境,应该已经就绪了,否则请安装Python。
可通过在命令行终端运行python3 --version
命令来进行确认。如果没有安装,请参考官方网站找到所使用的平台的安装包。
Ubuntu中通常预安装了 Python 3,如果没有安装,可通过以下命令进行安装:
1 |
sudo apt-get install python3 python3-pip |
如果你使用的是 Windows 10,可通过微软应用商店进行安装。
在PowerShell中运行python3会直接引导你去相应的下载页面(这算是龟叔去微软后增加的福利吗?)。
通过一键安装包安装了Odoo的Windows用户可能会奇怪为什么Python解释器没有准备就绪。这时需要进行额外的安装。简单地说是因为 Odoo一键安装包内置了Python解析器,在操作系统层面无法直接使用。
现在读者已经安装好了Python,就可以开始使用Odoo对外API了。
学习Odoo外部 API
在实现客户端应用前应当先熟悉下Odoo外部API。以下小节中使用Python解释器一探XML-RPC API。
使用XML-RPC连接 Odoo API
访问Odoo服务最简单的方法是使用XML-RPC,我们可以使用 Python 标准库中的xmlrpclib
来实现。
不要忘记我们是要编写客户端程序连接服务端,因此需运行 Odoo 服务端实例来供连接。本例中我们假设 Odoo 服务端实例在同一台机器上运行,http://localhost:8069,但读者可以使用任意运行着服务的其它机器,只需能连接其IP地址或服务器名。
Odoo的 xmlrpc/2/common端点暴露了公共方法,无需登录即可访问。可用于查看服务端版本及检测登录信息。我们使用xmlrpc库来研究对外可访问的Odoo API common。
首先打开 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': '15.0', 'server_version_info': [15, 0, 0, 'final', 0, ''], 'server_serie': '15.0', 'protocol_version': 1} |
以上代码导入了xmlrpc库,然后创建了一个包含服务端地址和监听端口信息的变量。请根据自身状况进行修改(如 Alan 使用srv = ‘http://192.168.0.12:8069’)。
下一步访问服务端公共服务(无需登录),在/xmlrpc/2/common端点上暴露。其中一个可用方法是version(),用于查看服务端版本。我们使用它来确认可与服务端进行通讯。
另一个公共方法是authenticate()。该方法确认用户名和密码可被接受,返回的用户 ID可用于后续请求。示例如下:
1 2 3 4 |
>>> db, user, password = "odoo-dev", "admin", "admin" >>> uid = common.authenticate(db, user, password, {}) >>> print(uid) 2 |
authenticate()方法接收4个参数:数据库名、用户名、密码以及user agent。些前的代码通过变量存储这些信息,然后将使用这些变量传参。
ODOO 14中发生的改变
Odoo 14支持API密钥,可使用它来获取Odoo API外部访问权限。API密钥可在用户的首选项(Preferences)表单中进行设置,位于账号安全(Account Security)标签下。
用户代理(User Agent)环境用于提供有关客户端的元信息。为必填项,至少应传一个空字典{}。
若验证失败,返回值为False。
common公共端点内容非常有限,要访问ORM API或是其它端点则需要先进行账号验证。
使用XML-RPC运行服务器端方法
要访问Odoo的模型及方法,需要使用xmlrpc/2/object。该端点要求先登录才能请求。
这个端点暴露了一个通用的execute_kw方法,接收模型名称、要调用的方法以及传递给方法的参数列表。
下面有一个演示execute_kw的示例。它调用了search_count方法,返回匹配域过滤器的记录数:
1 2 3 |
>>> api = client.ServerProxy('%s/xmlrpc/2/object' % srv) >>> api.execute_kw(db, uid, password, 'res.users', 'search_count', [[]]) 3 |
此处我们使用了xmlrpc/2/endpoint对象访问服务端 API。调用的方法名为execute_kw(),接收如下参数:
- 连接的数据库名
- 连接用户ID
- 用户密码(或API密钥)
- 目标模型名称
- 调用的方法
- 位置参数列表
- 可选的关键字参数字典(本例中未使用)
可调用所有的模型方法,以下划线(_)开头的除外,这些是私有方法。有些的方法的返回值如果无法通过XML-RPC发送,则无法使用XML-RPC协议调用。browse()方法就属于这种情况,它返回的是一个记录集对象。使用XML-RPC调用browse()会返回TypeError: cannot marshal objects的报错。在进行XML-RPC调用时应将browse()换成read或是search_read,所返回的数据格式可通过XML-RPC协议发送给客户端。
下面我们就来看看如何通过search和read查询Odoo数据。
使用API方法search和read
Odoo的服务端使用browse来查询记录。在RPC客户端中无法使用它,因为记录集对象无法通过RPC协议进行传输。这时应当使用read方法。
read([<ids>, [<fields>])和browse方法类似,但它返回的不是记录集,而是记录列表。每条记录都是包含请求字段及数据的字典。
下面来看如何通过read()从Odoo获取数据:
1 2 3 |
>>> api = client.ServerProxy("%s/xmlrpc/2/object" % srv) >>> api.execute_kw(db, uid, password, "res.users", "read", [2, ["login", "name", "company_id"]]) [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin', 'company_id': [1, 'YourCompany']}] |
上例对res.users模型调用了read方法,传入了两个位置参数:记录ID 2 (也可以使用ID列表)以及获取字段的列表[“login”, “name”, “company_id”],没传递关键字参数。
得到结果是一个字典列表,其中每个字典对应一条记录。对多字段的值有一种具体的表现形式。由记录ID和记录显示名组成的一对。例如,上例中返回的company_id的值为[1, ‘YourCompany’]。
可能会不知道记录ID,这时需要使用search调用来查找到匹配域过滤器的那些记录ID。
例如,想要查找管理员用户时,可使用[(“login”, “=”, “admin”)]。这一RPC调用如下:
1 2 3 |
>>> domain = [("login", "=", "admin")] >>> api.execute_kw(db, uid, password, "res.users", "search", [domain]) [2] |
其结果是仅有一个元素(2)的列表,元素为admin用户的ID。
经常会使用search结合read方法查找符合域过滤器条件的ID,然后再获取它们的数据。在客户端应用中,这会反复调用服务端。可通过search_read方法进行简化,它可以一步就执行以上两个操作。
下例为使用search_read来查找admin用户并返回其名称:
1 2 |
>>> api.execute_kw(db, uid, password, "res.users", "search_read", [domain, ["login", "name"]]) [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin'}] |
这个search_read方法使用了两个位置参数:包含域过滤器的列表,以及另一个包含需获取字段的列表。
search_read的参数如下:
- domain:域过滤器表达式列表
- fields:待获取字段名称列表
- offset:跳过的记录数或用于分页
- limit:返回的最大记录数
- order:用于ORDER BY语句的字符串
对read和search_read,fields均为可选参数。如未提供,会获取所有的模型字段。但这可能会使用到大开销的字段计算并且会返回大量无需使用的数据。因此建议显式地提供字段列表。
execute_kw调用既可使用位置参数也可使用关键字参数。以下是把位置参数换成关键字参数的示例:
1 2 |
>>> api.execute_kw(db, uid, password, "res.users", "search_read", [], {"domain": domain, "fields": ["login", "name"]}) [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin'}] |
获取数据时最常使用的就是search_read,但还存在其它方法用于写入数据或触发其它业务逻辑。
调用其它API方法
所有的其它模型方法也通过RPC对外暴露,那以下划线开头的私有方法除外。也就是说可以调用create、write和unlink来修改服务端的数据。
我们来看一个例子。以下代码新建一条partner记录,然后修改记录,再读取记录确定是否写入了修改,最后进行了删除:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
>>> x = api.execute_kw(db, uid, password, "res.partner", ... "create", ... [{'name': 'Packt Pub'}]) >>> print(x) 63 >>> api.execute_kw(db, uid, password, "res.partner", ... "write", ... [[x], {'name': 'Packt Publishing'}]) True >>> api.execute_kw(db, uid, password, "res.partner", ... "read", ... [[x], ["name"]]) [{'id': 63, 'name': 'Packt Publishing'}] >>> api.execute_kw(db, uid, password, "res.partner", ... "unlink", ... [[x]]) True >>> api.execute_kw(db, uid, password, "res.partner", ... "read", ... [[x]]) [] |
XML-RPC的一个限制是它不支持None值。有一个XML-RPC插件可支持None值,但这取决于客户端使用的XML-RPC库。没有返回值的方法可能无法使用XML-RPC,因为这些方法隐式地返回None。这也是为什么方法的最佳实践要求有返回值,至少应返回True。另一个选择是使用JSON-RPC。OdooRPC库支持该协议,本文的使用OdooRPC库一节中会用到它。
模型中以下划线开头的方法被看作私有方法,无法通过XML-RPC对外暴露。
小贴士:通常客户端应用希望复制用户在Odoo表单中输入的内容。调用create()方法可能还不够,因为表单可能会使用onchange方法自动化操作一些字段,这通过表单交互触发,没经过create()。解决方案是在Odoo中创建一个自定义方法,其中使用create()方法并运行onchange方法中的操作。
有必须反复说明一下,Odoo的对外API大部分编程语言都可以使用。官方文档中包含有Ruby、PHP和Java使用示例。
至此,我们已学习到如何使用XML-RPC协议调用Odoo方法。接下来我们使用它来构建图书目录客户端应用。
实现图书客户端XML-RPC 接口
下面就来实现图书目录客户端应用。
可分为两个文件:一个是包含服务端后台的Odoo后台接口,library_xmlrpc.py,另一个用于用户界面,library.py。这让我们可以对后台接口使用替代的实现。
先从Odoo后台组件开始,LibraryAPI类用于配置与Odoo服务端之间连接,以支持与Odoo交互所需的方法。所要实现的方法有:
- search_read(<title>) 通过标题查找图书数据
- create(<title>) 使用指定标题创建图书
- write(<id>, <title>) 使用图书ID更新书名
- unlink(<id>) 使用ID删除图书
在电脑上选择一个目录存放应用文件,并创建library_xmlrpc.py文件。先添加类的构造方法,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import xmlrpc.client class LibraryAPI: def __init__(self, host, port, db, user, pwd): common = xmlrpc.client.ServerProxy( "http://%s:%d/xmlrpc/2/common" % (host, port)) self.api = xmlrpc.client.ServerProxy( "http://%s:%d/xmlrpc/2/object" % (host, port)) self.uid = common.authenticate(db, user, pwd, {}) self.pwd = pwd self.db = db self.model = "library.book" |
类中存储了执行对目标模块调用所需的所有信息:API XML-RPC引用、uid、密码、数据库名以及模型名。
对Odoo的RPC调用会使用相同的execute_kw RPC方法。下面对其添加一层封装,放在_execute() 私有方法中。它利用对象存储的数据提供更小的函数签名,如以下代码所示:
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 {}) |
_execute()私有方法用于让更高阶的方法实现更简洁。
第一个公有方法是search_read()。它接收 一个可选字符串用于搜索书名。如未提供标题,则会返回所有记录。相应的实现如下:
1 2 3 4 |
def search_read(self, title=None): domain = [("name", "ilike", title)] if title 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, id, title): vals = {"name": title} return self._execute("write", [[id], vals]) |
最后unlink()方法用于删除给定ID的图书:
1 2 |
def unlink(self, id): return self._execute("unlink", [[id]]) |
在该Python文件最后添加一段测试代码在运行时执行:
1 2 3 4 5 6 7 8 |
if __name__ == "__main__": # 测试配置 host, port, db = "localhost", 8069, "odoo-dev" user, pwd = "admin", "admin" api = LibraryAPI(host, port, db, user, pwd) from pprint import pprint pprint(api.search_read()) |
如果执行以上 Python 脚本,我们可以打印出图书的内容:
1 2 3 4 |
$ python3 library_xmlrpc.py [{'id': 3, 'name': 'Brave New World'}, {'id': 2, 'name': 'Odoo 11 Development Cookbook'}, {'id': 1, 'name': 'Odoo Development Essentials 11'}] |
现在已经有了对 Odoo 后台的简单封装,下面就可以处理命令行用户接口了。
实现客户端用户接口
我的目标是学习如何写外部应用和 Odoo 服务之间的接口,前面已经实现了。但不能止步于此,我们再为这个最小化客户端应用构建一个用户接口。
为保持尽量简单,我们使用简单的命令行用户接口并避免使用其它依赖。那我们可以使用Python 内置功能和ArgumentParser库来实现这个命令行应用。代码如下:
1 2 |
from argparse import ArgumentParser from library_xmlrpc import LibraryAPI |
下面我们来看看参数解析器接收的命令,有以下四条命令:
- list 搜索并列出图书
- add 添加图书
- set 修改书名
- del 删除图书
实现以上命令的命令行解析代码如下:
1 2 3 4 5 6 |
parser = ArgumentParser() parser.add_argument( "command", choices=["list", "add", "set", "del"]) parser.add_argument("params", nargs="*") # 可选参数 args = parser.parse_args() |
这里的args对象表示用户传入的参数。args.command是所用到的命令,在给定args.params时,其存储的是命令所使用的其它参数。
如果未传参数或参数错误,参数解析器会进行处理,提示用户应该输入的内容。有关argparse更完整的说明,请参考官方文档。
下一步是执行操作响应用户输入的命令。我们先创建一个LibraryAPI实例。这需要提供详细的Odoo连接信息,在我们的简单实现中采用了硬编码,代码如下:
1 2 3 |
host, port, db = "localhost", 8069, "odoo-dev" user, pwd = "admin", "admin" api = LibraryAPI(host, port, db, user, pwd) |
第一行代码设置服务实例的一些固定参数以及要连接的数据库。本例中,我们连接本地 Odoo 服务(localhost),监听8069默认端口,并使用 odoo-dev数据库。如需连接其它服务器和数据库,请对参数进行相应调整。
还需要添加代码处理每条命令。我们先从返回图书列表的list命令开始:
1 2 3 4 5 6 7 |
if args.command == "list": title = args.params[:1] if len(title) != 0: title = title[0] books = api.search_read(title) for book in books: print("%(id)d %(name)s" % book) |
这里我们使用了LibraryAPI.search_read()来从服务端获取图书记录列表。然后遍历列表中每个元素并打印。
下面添加add命令:
1 2 3 4 |
if args.command == "add": title = args.params[0] book_id = api.create(title) print("Book added with ID %d for title %s." % (book_id, title)) |
因为主要的工作已经在LibraryAPI对象中完成,我们只要调用create() 方法并向终端用户显示结果即可。
set命令允许我们修改已有图书的书名,应传入两个参数,新书名和图书的 ID:
1 2 3 4 5 6 7 |
if args.command == "set": 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(book_id, title) print("Title of Book ID %d set to %s." % (book_id, title)) |
最终我们要实现 del 命令来删除图书记录。实现方式和之前并没有什么差别:
1 2 3 4 |
if args.command == "del": book_id = int(args.params[0]) api.unlink(book_id) print("Book with ID %s was deleted." % book_id) |
客户端应用至此已完成,可以尝试使用一些命令。应该可以执行本文开头的那些命令。
小贴士:在Linux系统中,可通过执行chmod +x library.py命令并在文件的首行添加#!/usr/bin/env python3来让library.py文件变为可执行。之后就可以在命令行中运行了./library.py。
这是一个非常基础的应用,还有很多改进的方式。我们的目的是使用Odoo RPC API构建一个最小可用应用。
使用OdooRPC库
另一个可以考虑的客户端库是OdooRPC。它是一个完整的客户端库,把XML-RPC协议换成了JSON-RPC 协议。事实上 Odoo 官方客户端使用的就是JSON-RPC,XML-RPC更多是用于支持向后兼容性。
ℹ️OdooRPC库现在由 OCA 管理和持续维护。了解更多请参见OCA。
OdooRPC库可通过PyPI安装:
1 |
pip3 install odoorpc |
OdooRPC在新建odoorpc.ODOO对象时配置了服务端连接。此时我们应使用ODOO.login()方法创建一个用户会话。和服务端一样,会员有一个包含会话环境的env属性,包括用户ID、uid和上下文。
OdooRPC库可用于对服务端的library_xmlrpc.py接口提供一个替代实现。功能相同,只是把XML-RPC换成了JSON-RPC。
创建library_odoorpc.py Python模块来对library_xmlrpc.py模块进行修改。新建的library_odoorpc.py文件中加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import odoorpc class LibraryAPI(): def __init__(self, host, port, db, user, pwd): self.api = odoorpc.ODOO(host, 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属性中存储对library.book模型引用,通过OdooRPC的env[“library.book”]调用提供。
这里同样实现了_execute()方法,可与XML-RPC版本进行对比。OdooRPC库中的execute()方法可运行指定的Odoo模型方法。
下面我们来实现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, title=None): domain = [("name", "ilike", title)] if title 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, id, title): vals = {"name": title} self.Model.write(id, vals) def unlink(self, id): return self.Model.unlink(id) |
注意这段代码和 Odoo 服务端代码极其相似。
可使用LibraryAPI对象替换library_xmlrpc.py。可通过编辑library.py文件将from library_xmlrpc import LibraryAPI一行替换为from library_odoorpc import LibraryAPI将其用作RPC连接层。然后对library.py客户端应用进行测试,执行效果应该是和之前一样的。
了解ERPpeek客户端
ERPpeek是一个多功能工具,既可以作为交互式命令行接口(CLI)也可以作为 Python库,它提供了比xmlrpc库更便捷的 API。它在PyPi索引中,可通过如下命令安装:
1 |
pip3 install 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://127.0.0.1:8069' -d odoo-dev -uadmin Usage (some commands): models(name) # List models matching pattern model(name) # Return a Model instance ... Password for 'admin': Logged in as 'admin' odoo-dev >>> model('res.users').count() 3 odoo-dev >>> rec = model('res.partner').browse(14) odoo-dev >>> rec.name 'Azure Interior' |
如上所见,建立了服务端的连接,执行上下文引用了model() 方法来获得模型实例并对其进行操作。连接使用的erppeek.Client实例也可通过客户端变量来使用。 值得一提的是它可替代网页客户端来管理所安装的插件模块:
- client.modules()列出可用或已安装模块
- client.install()执行模块安装
- client.upgrade()执行模块升级
- client.uninstall()卸载模块
因此ERPpeek可作为 Odoo 服务端远程管理的很好的服务。有关ERPpeek的更多细节请见 GitHub。
小结
本文的目标是学习外部 API 如何运作以及它们能做些什么。一开始我们通过一个简单的Python XML-RPC客户端来进行探讨,但外部 API 也可用于其它编程语言。事实上官方文档中包含了Java, PHP和Ruby的代码示例。
然后我们学习了如何使用XML-RPC调用搜索、读取数据,以及如何调用其它方法。比如我们可以创建、更新和删除记录。
接着我们介绍了OdooRPC库。它在RPC基础库(XML-RPC 或 JSON-RPC) 上提供了一层,用于提供类似服务端API的本地API。这降低了学习曲线,减少了编程失误并且让在服务端和客户端之间拷贝代码变得更容易。
以上我们就完结了本文有关编程 API 和业务逻辑的学习。是时候深入视图和用户界面了。在下一篇文章中,我们进一步学习后台视图以及web客户端提供的开箱即用的用户体验。
扩展阅读
以下参考资料可用于补充本文所学习的内容: