来进行定义,其中 N 表示占用列的数量。
212 |
213 | > **小贴士:**Bootstrap 4 在其大部分构件中使用了 CSS 弹性盒子布局,已知wkhtmltopdf 对弹性盒子的功能并不都能很好的支持。因此如果有些地方效果不对,请尝试使用其它元素或方法,如 HTML 表格。
214 |
215 | 
216 |
217 | 此处我们为头部行添加了标题,然后t-foreach循环遍历每条记录并在各行中进行渲染。因为渲染由服务端完成,记录都是对象,我们可使用点号标记来从关联数据记录中访问字段。这也让关联字段的数据访问变得更为容易。注意这在客户端渲染的QWeb视图中是无法使用的,比如网页客户端的看板视图。
218 |
219 | 以下是在
元素中的记录行内容XML:
220 |
221 | ```
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
232 |
233 |
234 |
239 |
240 |
241 |
242 |
243 | ```
244 |
245 | 可以看到字段可通过t-options属性添加额外的选项,内容为包含带有widget键的 JSON 字典。更为复杂的示例是contact组件,用于格式化地址。我们使用它来渲染出版商地址o.publisher_id。默认contact 组件显示地址时带有图像,类似电话图标。no_marker="true"选项禁用了这一显示。
246 |
247 | 
248 |
249 | 补充:no_marker="true"禁用的地址图标如上所示
250 |
251 | ## 渲染图片
252 |
253 | 我们报表最后一列为一组带有头像的作者。我们将通过遍历来渲染出每个作者,并使用Bootstrap媒体对象:
254 |
255 | ```
256 |
257 |
258 |
259 | -
260 |
262 |
267 |
268 |
269 |
270 | ```
271 |
272 | 此处我们遍历了author_ids,使用字段图像组件
对每个作者的头像进行了渲染,然后还有姓名。
273 |
274 | 
275 |
276 | 注:以上头像来自各位大神 Twitter 的真实头像
277 |
278 | ## 报表汇总
279 |
280 | 报表中经常需要提供汇总。这可借由 Python 表达式来计算总额。在闭合标签之后,我们添加最后一行用于汇总:
281 |
282 | ```
283 |
284 |
285 |
286 | Count:
287 |
288 |
289 |
290 |
291 |
292 |
293 | ```
294 |
295 | len() Python函数用于计算集合元素的数量。类似地,汇总也可以使用sum()来对一组值进行求和运算。例如,可使用如下列表推导式来进行总额运算:
296 |
297 | ```
298 |
299 | ```
300 |
301 | 可以把这个列表推导式看作一个内嵌的循环。有时我们需要贯穿报表执行一些计算,如流动汇总(running total),汇总至当前记录。这可通过t-set 来定义累加变量在每一行进行更新来实现。为描述这一功能,我们来计算作者数的累加。首先在docs 记录集 t-foreach 循环前初始化变量:
302 |
303 | ```
304 |
305 |
306 | ```
307 |
308 | 然后在循环内,将记录的作者数添加到变量中。我们这里显示在书名之后,并在每行打印出当前总数:
309 |
310 | ```
311 |
312 |
313 | (Accum. authors: )
314 | ```
315 |
316 | 
317 |
318 | ## 定义纸张样式
319 |
320 | 到这里我们的报表的 HTML 显示没有问题了,但在打印的 PDF 页面中还不够美观。使用横向页面显示结果会更好,因此下面就来添加纸张样式。在报表 XML 文件的最上方添加如下代码:
321 |
322 | ```
323 |
325 | European A4 Landscape
326 |
327 | A4
328 | 0
329 | 0
330 | Landscape
331 | 40
332 | 23
333 | 7
334 | 7
335 |
336 | 35
337 | 90
338 |
339 | ```
340 |
341 | 这是对European A4格式的一个拷贝,这在data/report_paperformat_data.xml文件中定义的base 模块中,但这里将排列方向由纵向改为了横向。定义的纸张样式可通过后台Settings > Technical > Reporting > Paper Format菜单进行查看。
342 |
343 | 
344 |
345 | 现在就可在报表中使用它了。默认的纸张样式在公司设置中定义,但我们也可以为特定报表指定纸张样式。这通过在报表操作中的paperfomat属性来实现。下面来编辑打开报表使用的操作,添加这一属性:
346 |
347 | ```
348 |
351 | ```
352 |
353 | 
354 |
355 | ## 在报表中启用语言翻译
356 |
357 | 要在报表中启用翻译,需要使用带有t-lang属性的元素在模板中调用翻译方法。t-lang需传入一个语言代码来运行,如es或en_US。它需要可以找到所需使用语言的字段名。一种方式是使用当前用户的语言,为此,我们定义一个外层翻译报表来调用要翻译的报表,使用t-lang属性来设置语言来源:
358 |
359 | ```
360 |
364 |
365 |
366 |
368 |
369 | ```
370 |
371 | 本例中,每本书都使用了用户的语言user_id.lang来进行渲染。
372 |
373 | 有些情况下,我们可能需要每条记录以指定语言进行渲染。比如在销售订单中,我们可能要各条记录按照对应合作方/客户的首选语言进行打印。假设我们需要每本书按照对应出版商的语言进行渲染,QWeb模板可以这么写:
374 |
375 | ```
376 |
377 |
378 |
380 |
381 |
382 |
383 |
384 | ```
385 |
386 | 以上我们对记录进行了迭代,然后每条记录根据记录上的数据使用相应的语言进行报表模板的调用,本例为出版商的语言publisher_id.lang。
387 |
388 | 补充:以上代码运行时每条记录都会带有一个头部,如需按列表显示还需将头部抽象到循环之外
389 |
390 | ## 使用自定义 SQL 创建报表
391 |
392 | 前面我们所创建的报表都是基于常规记录集,但在有些情况下我们需要执行一些在QWeb模板中不易于处理的数据转换或累加。一种解决方法是写原生 SQL 查询来创建我们所需的数据集,将结果通过特殊的模型进行暴露,然后基于这一数据集来生成报表。
393 |
394 | 我们创建reports/library_book_report.py文件来讲解这一情况,代码如下:
395 |
396 | ```
397 | from odoo import models, fields
398 |
399 | class BookReport(models.Model):
400 | _name = 'library.book.report'
401 | _description = 'Book Report'
402 | _auto = False
403 |
404 | name = fields.Char('Title')
405 | publisher_id = fields.Many2one('res.partner')
406 | date_published = fields.Date()
407 |
408 | def init(self):
409 | self.env.cr.execute("""
410 | CREATE OR REPLACE VIEW library_book_report AS
411 | (SELECT *
412 | FROM library_book
413 | WHERE active=True)
414 | """)
415 | ```
416 |
417 | 要加载以上文件,需要在模块的顶级__init__.py文件中添加from . import reports,并在reports/__init__.py文件中添加from . import library_book_report。
418 |
419 | _auto属性用于阻止数据表的自动创建。我们在模型的init()方法中添加了替代的 SQL。它会创建数据库视图,并提供报表所需数据。以上 SQL 查询非常简单,旨在说明我们为视图可以使用任意有效的 SQL查询,如对额外数据执行累加或计算。
420 |
421 | 我们还需要声明模型字段来让 Odoo 知道如何正确处理每一条记录中的数据。同时不要忘记为新模型添加安全访问规则,否则将无法使用该模型。下面在security/ir.model.access.csv文件中添加下行:
422 |
423 | ```
424 | access_library_book_report,access_library_book_report,model_library_book_report,
425 | library_group_user,1,0,0,0
426 | ```
427 |
428 | 还应注意这是一个全新的不同模型,与图书模型的访问规则并不相同。下一步基于该模型我们可以使用reports/library_book_sql_report.xml新增一个报表:
429 |
430 | ```
431 |
432 |
433 |
438 |
439 |
440 |
441 |
442 |
443 |
444 |
445 |
446 | | Title |
447 | Published |
448 | Date |
449 |
450 |
451 |
452 | |
453 |
454 | |
455 |
456 |
457 | |
458 |
459 |
461 | |
462 |
463 |
464 |
465 |
466 |
467 |
468 |
469 |
470 | ```
471 |
472 | 对于更为复杂的情况,我们还会需要用户输入参数,这时可以使用不同的方案:向导。为此我们应创建一个临时模型来存储用户的报表参数。因为这是由代码生成的,所以我们可以使用所需的任意逻辑。
473 |
474 | 强烈推荐学习已有的相似报表来获取灵感。一个不错的例子是Leaves菜单选项下的Leaves by Department,相应的临时模型定义可以参见addons/hr_holidays/wizard/hr_holidays_summary_employees.py。
475 |
476 | 补充:原书未对这一部分进行验证,Alan 下面通过添加菜单项的方式来进行验证,还有其它方式,欢迎读者留言讨论
477 |
478 | 首先在views/library_menu.xml文件中添加如下内容:
479 |
480 | ```
481 |
486 |
491 | ```
492 |
493 | 在__manifest__.py 文件中引入前述的 XML 文件后更新模块
494 |
495 | 
496 |
497 | 
498 |
499 | ## 总结
500 |
501 | 前面一篇文章中,我们学习了QWeb以及如何使用它来设计看板视图。本文中我们学习了QWeb报表引擎,以及使用QWeb模板语言创建报表最为重要的一些技术。
502 |
503 | 下一篇文章中,我们将继续使用QWeb,这次是创建网页。我们将学习书写网页控制器,为我们的网页提供更丰富的功能。
504 |
505 |
506 |
507 | ☞☞☞第十三章 [Odoo 12开发之创建网站前端功能](13.md)
508 |
509 |
510 |
511 | ## 扩展阅读
512 |
513 | 本文所学习课题的补充参考材料有:
514 |
515 | - Odoo官方文档对应专区:
516 | - [报表](https://www.odoo.com/documentation/12.0/reference/reports.html)
517 | - [QWeb语言](https://www.odoo.com/documentation/12.0/reference/qweb.html)
518 | - [Bootstrap样式文档](https://getbootstrap.com/docs/4.1/getting-started/introduction/)
--------------------------------------------------------------------------------
/9.md:
--------------------------------------------------------------------------------
1 | # 第九章 Odoo 15开发之外部 API - 集成第三方系统
2 |
3 | Odoo 服务端提供有外部 API,可供网页客户端和其它客户端应用使用。本章中我们将学习如何Odoo 的外部 API来实现将其Odoo服务端作为后端的外部应用。
4 |
5 | 可通过编写脚本来加载或修改Odoo数据,或是集成Odoo现有的业务应用,作为对Odoo应用一种补充。
6 |
7 | 我们将描述如何使用Odoo RPC调用,然后根据所学知识使用 Python为图书应用创建一个简单的命令行应用。
8 |
9 | 本章的主要内容有:
10 |
11 | * 介绍学习项目:图书目录的客户端应用
12 | * 在客户端机器上配置 Python
13 | * 探索Odoo的外部API
14 | * 实现客户端应用的XML-RPC接口
15 | * 实现客户端应用的用户界面
16 | * 使用OdooRPC库
17 |
18 | 学完本章后,读者可以创建一个简单的Python应用,使用Odoo作为后端进行查询和数据存储。
19 |
20 | ## 开发准备
21 |
22 | 本文基于[第三章 Odoo 15开发之创建第一个 Odoo 应用](3.md)创建的代码,具体代码请参见 [GitHub 仓库](https://github.com/iTranslateX/odoo-essentials/tree/main/source-code)。应将library_app模块放在addons路径下并进行安装。为保持前后一致,我们将使用[第二章 Odoo 15开发之开发环境准备](2.md)中的安装操作。本章完成后的代码请参见 [GitHub 仓库](https://github.com/iTranslateX/odoo-essentials/tree/main/source-code/ch09)。
23 |
24 | ## 学习项目-图书目录客户端
25 |
26 | 本文中,我们将开发一个简单的客户端应用来管理图书目录。这是一个命令行接口(CLI) 应用,使用 Odoo 来作为后端。应用的功能很基础,核心放在用于与 Odoo服务端交互的技术。
27 |
28 | 这个简单CLI应用可以完成如下功能:
29 |
30 | * 通过标题搜索并列出图书
31 | * 向目录添加新书籍
32 | * 编辑图书标题
33 |
34 | 我们的主要目标是使用Odoo对外API,因此不希望引用其它读者可能不太熟悉的编程语言。有了这一出发点,最好的方式是就是使用Python来实现客户端应用。不过只要掌握了一种语言的XML-RPC库,相关处理RPC的技术同样适用于其它语言。
35 |
36 | 这个应用是一个 Python 脚本,等待输入命令来执行操作。示例如下:
37 |
38 | ```
39 | $ python3 library.py add "Moby-Dick"
40 | $ python3 library.py list "moby"
41 | 60 Moby-Dick
42 | $ python3 library.py set 60 "Moby Dick"
43 | ```
44 |
45 | 这个示例会话演示了如何使用客户端应用添加、列出及修改图书标题。
46 |
47 | 该客户端应用通过Python运行,在开始编写客户端应用代码之前,应确保在客户端机器上安装有Python。
48 |
49 | ## 在客户端机器上安装 Python
50 |
51 | Odoo API 可以在外部通过两种协议访问:XML-RPC和JSON-RPC。任何外部程序,只要是能实现其中一种协议的客户端,就可以与 Odoo 服务端进行交互。为避免引入其它编程语言,我们将保持使用 Python 来探讨外部 API。
52 |
53 | 到目前为止我们仅在服务端运行了 Python 代码。现在我们要在客户端上使用 Python,所以你可能需要在电脑上做一些额外设置。
54 |
55 | 要学习本文的示例,你需要能在操作电脑上运行 Python 3 代码。如果在前面已按前面章节配置过开发环境,应该已经就绪了,否则请安装Python。
56 |
57 | 可通过在命令行终端运行`python3 --version`命令来进行确认。如果没有安装,请参考官方网站找到所使用的平台的[安装包](https://www.python.org/downloads/)。
58 |
59 | Ubuntu中通常预安装了 Python 3,如果没有安装,可通过以下命令进行安装:
60 |
61 | ```
62 | sudo apt-get install python3 python3-pip
63 | ```
64 |
65 | 如果你使用的是 Windows 10,可通过微软应用商店进行安装。
66 |
67 | 在PowerShell中运行**python3**会直接引导你去相应的下载页面(这算是龟叔去微软后增加的福利吗?)。
68 |
69 | 通过一键安装包安装了Odoo的Windows用户可能会奇怪为什么Python解释器没有准备就绪。这时需要进行额外的安装。简单地说是因为 Odoo一键安装包内置了Python解析器,在操作系统层面无法直接使用。
70 |
71 | 现在读者已经安装好了Python,就可以开始使用Odoo对外API了。
72 |
73 | ## 学习Odoo外部 API
74 |
75 | 在实现客户端应用前应当先熟悉下Odoo外部API。以下小节中使用*Python*解释器一探XML-RPC API。
76 |
77 | ### 使用XML-RPC连接 Odoo API
78 |
79 | 访问Odoo服务最简单的方法是使用XML-RPC,我们可以使用 Python 标准库中的`xmlrpclib`来实现。
80 |
81 | 不要忘记我们是要编写客户端程序连接服务端,因此需运行 Odoo 服务端实例来供连接。本例中我们假设 Odoo 服务端实例在同一台机器上运行,**http://localhost:8069** ,但读者可以使用任意运行着服务的其它机器,只需能连接其IP地址或服务器名。
82 |
83 | Odoo的 **xmlrpc/2/common**端点暴露了公共方法,无需登录即可访问。可用于查看服务端版本及检测登录信息。我们使用**xmlrpc**库来研究对外可访问的Odoo API **common**。
84 |
85 | 首先打开 Python 3终端并输入如下代码:
86 |
87 | ```
88 | >>> from xmlrpc import client
89 | >>> srv = "http://localhost:8069"
90 | >>> common = client.ServerProxy("%s/xmlrpc/2/common" % srv)
91 | >>> common.version()
92 | {'server_version': '15.0', 'server_version_info': [15, 0, 0, 'final', 0, ''], 'server_serie': '15.0', 'protocol_version': 1}
93 | ```
94 |
95 | 以上代码导入了**xmlrpc**库,然后创建了一个包含服务端地址和监听端口信息的变量。请根据自身状况进行修改(如 Alan 使用`srv = 'http://192.168.0.12:8069'`)。
96 |
97 | 下一步访问服务端公共服务(无需登录),在 **/xmlrpc/2/common** 端点上暴露。其中一个可用方法是**version()**,用于查看服务端版本。我们使用它来确认可与服务端进行通讯。
98 |
99 | 另一个公共方法是**authenticate()**。该方法确认用户名和密码可被接受,返回的用户 ID可用于后续请求。示例如下:
100 |
101 | ```
102 | >>> db, user, password = "odoo-dev", "admin", "admin"
103 | >>> uid = common.authenticate(db, user, password, {})
104 | >>> print(uid)
105 | 2
106 | ```
107 |
108 | **authenticate()**方法接收4个参数:数据库名、用户名、密码以及user agent。些前的代码通过变量存储这些信息,然后将使用这些变量传参。
109 |
110 | > ODOO 14中发生的改变
111 | >
112 | > Odoo 14支持API密钥,可使用它来获取Odoo API外部访问权限。API密钥可在用户的首选项(**Preferences**)表单中进行设置,位于账号安全(**Account Security**)标签下。
113 |
114 | 用户代理(User Agent)环境用于提供有关客户端的元信息。为必填项,至少应传一个空字典 **{}**。
115 |
116 | 若验证失败,返回值为**False**。
117 |
118 | **common**公共端点内容非常有限,要访问ORM API或是其它端点则需要先进行账号验证。
119 |
120 |
121 | 
122 |
123 | ### 使用XML-RPC运行服务器端方法
124 |
125 | 要访问Odoo的模型及方法,需要使用**xmlrpc/2/object**。该端点要求先登录才能请求。
126 |
127 | 这个端点暴露了一个通用的**execute_kw**方法,接收模型名称、要调用的方法以及传递给方法的参数列表。
128 |
129 | 下面有一个演示**execute_kw**的示例。它调用了**search_count**方法,返回匹配域过滤器的记录数:
130 |
131 | ```
132 | >>> api = client.ServerProxy('%s/xmlrpc/2/object' % srv)
133 | >>> api.execute_kw(db, uid, password, 'res.users', 'search_count', [[]])
134 | 3
135 | ```
136 |
137 | 此处我们使用了**xmlrpc/2/endpoint**对象访问服务端 API。调用的方法名为**execute_kw()**,接收如下参数:
138 |
139 | * 连接的数据库名
140 | * 连接用户ID
141 | * 用户密码(或API密钥)
142 | * 目标模型名称
143 | * 调用的方法
144 | * 位置参数列表
145 | * 可选的关键字参数字典(本例中未使用)
146 |
147 | 可调用所有的模型方法,以下划线(**_**)开头的除外,这些是私有方法。有些的方法的返回值如果无法通过XML-RPC发送,则无法使用XML-RPC协议调用。**browse()**方法就属于这种情况,它返回的是一个记录集对象。使用XML-RPC调用**browse()**会返回**TypeError: cannot marshal objects**的报错。在进行XML-RPC调用时应将**browse()**换成**read**或是**search_read**,所返回的数据格式可通过XML-RPC协议发送给客户端。
148 |
149 | 下面我们就来看看如何通过**search**和**read**查询Odoo数据。
150 |
151 | ### 使用API方法**search**和**read**
152 |
153 | Odoo的服务端使用**browse**来查询记录。在RPC客户端中无法使用它,因为记录集对象无法通过RPC协议进行传输。这时应当使用**read**方法。
154 |
155 | **read([, [])**和**browse**方法类似,但它返回的不是记录集,而是记录列表。每条记录都是包含请求字段及数据的字典。
156 |
157 | 下面来看如何通过**read()** 从Odoo获取数据:
158 |
159 | ```
160 | >>> api = client.ServerProxy("%s/xmlrpc/2/object" % srv)
161 | >>> api.execute_kw(db, uid, password, "res.users", "read", [2, ["login", "name", "company_id"]])
162 | [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin', 'company_id': [1, 'YourCompany']}]
163 | ```
164 |
165 | 上例对**res.users**模型调用了**read**方法,传入了两个位置参数:记录ID **2** (也可以使用ID列表)以及获取字段的列表 **["login", "name", "company_id"]**,没传递关键字参数。
166 |
167 | 得到结果是一个字典列表,其中每个字典对应一条记录。对多字段的值有一种具体的表现形式。由记录ID和记录显示名组成的一对。例如,上例中返回的**company_id**的值为 **[1, 'YourCompany']**。
168 |
169 | 可能会不知道记录ID,这时需要使用**search**调用来查找到匹配域过滤器的那些记录ID。
170 |
171 | 例如,想要查找管理员用户时,可使用 **[("login", "=", "admin")]**。这一RPC调用如下:
172 |
173 | ```
174 | >>> domain = [("login", "=", "admin")]
175 | >>> api.execute_kw(db, uid, password, "res.users", "search", [domain])
176 | [2]
177 | ```
178 |
179 | 其结果是仅有一个元素(**2**)的列表,元素为**admin**用户的ID。
180 |
181 | 经常会使用**search**结合**read**方法查找符合域过滤器条件的ID,然后再获取它们的数据。在客户端应用中,这会反复调用服务端。可通过**search_read**方法进行简化,它可以一步就执行以上两个操作。
182 |
183 | 下例为使用**search_read**来查找admin用户并返回其名称:
184 |
185 | ```
186 | >>> api.execute_kw(db, uid, password, "res.users", "search_read", [domain, ["login", "name"]])
187 | [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin'}]
188 | ```
189 |
190 | 这个**search_read**方法使用了两个位置参数:包含域过滤器的列表,以及另一个包含需获取字段的列表。
191 |
192 | **search_read**的参数如下:
193 |
194 | * **domain:** 域过滤器表达式列表
195 | * **fields:** 待获取字段名称列表
196 | * **offset:** 跳过的记录数或用于分页
197 | * **limit:** 返回的最大记录数
198 | * **order:** 用于**ORDER BY**语句的字符串
199 |
200 | 对**read**和**search_read**,**fields**均为可选参数。如未提供,会获取所有的模型字段。但这可能会使用到大开销的字段计算并且会返回大量无需使用的数据。因此建议显式地提供字段列表。
201 |
202 | **execute_kw**调用既可使用位置参数也可使用关键字参数。以下是把位置参数换成关键字参数的示例:
203 |
204 | ```
205 | >>> api.execute_kw(db, uid, password, "res.users", "search_read", [], {"domain": domain, "fields": ["login", "name"]})
206 | [{'id': 2, 'login': 'admin', 'name': 'Mitchell Admin'}]
207 | ```
208 |
209 | 获取数据时最常使用的就是**search_read**,但还存在其它方法用于写入数据或触发其它业务逻辑。
210 |
211 | ### 调用其它API方法
212 |
213 | 所有的其它模型方法也通过RPC对外暴露,那以下划线开头的私有方法除外。也就是说可以调用**create**、**write**和**unlink**来修改服务端的数据。
214 |
215 | 我们来看一个例子。以下代码新建一条partner记录,然后修改记录,再读取记录确定是否写入了修改,最后进行了删除:
216 |
217 | ```
218 | >>> x = api.execute_kw(db, uid, password, "res.partner",
219 | ... "create",
220 | ... [{'name': 'Packt Pub'}])
221 | >>> print(x)
222 | 63
223 | >>> api.execute_kw(db, uid, password, "res.partner",
224 | ... "write",
225 | ... [[x], {'name': 'Packt Publishing'}])
226 | True
227 | >>> api.execute_kw(db, uid, password, "res.partner",
228 | ... "read",
229 | ... [[x], ["name"]])
230 | [{'id': 63, 'name': 'Packt Publishing'}]
231 | >>> api.execute_kw(db, uid, password, "res.partner",
232 | ... "unlink",
233 | ... [[x]])
234 | True
235 | >>> api.execute_kw(db, uid, password, "res.partner",
236 | ... "read",
237 | ... [[x]])
238 | []
239 | ```
240 |
241 | XML-RPC的一个限制是它不支持**None**值。有一个XML-RPC插件可支持**None**值,但这取决于客户端使用的XML-RPC库。没有返回值的方法可能无法使用XML-RPC,因为这些方法隐式地返回**None**。这也是为什么方法的最佳实践要求有返回值,至少应返回**True**。另一个选择是使用JSON-RPC。**OdooRPC**库支持该协议,本文的**使用OdooRPC库**一节中会用到它。
242 |
243 | 模型中以下划线开头的方法被看作私有方法,无法通过XML-RPC对外暴露。
244 |
245 | > **小贴士**:通常客户端应用希望复制用户在Odoo表单中输入的内容。调用**create()**方法可能还不够,因为表单可能会使用**onchange**方法自动化操作一些字段,这通过表单交互触发,没经过**create()**。解决方案是在Odoo中创建一个自定义方法,其中使用**create()**方法并运行**onchange**方法中的操作。
246 |
247 | 有必须反复说明一下,Odoo的对外API大部分编程语言都可以使用。[官方文档](https://www.odoo.com/documentation/15.0/developer/api/external_api.html)中包含有Ruby、PHP和Java使用示例。
248 |
249 | 至此,我们已学习到如何使用XML-RPC协议调用Odoo方法。接下来我们使用它来构建图书目录客户端应用。
250 |
251 | ## 实现图书客户端XML-RPC 接口
252 |
253 | 下面就来实现图书目录客户端应用。
254 |
255 | 可分为两个文件:一个是包含服务端后台的Odoo后台接口,**library_xmlrpc.py**,另一个用于用户界面,**library.py**。这让我们可以对后台接口使用替代的实现。
256 |
257 | 先从Odoo后台组件开始,**LibraryAPI** 类用于配置与Odoo服务端之间连接,以支持与Odoo交互所需的方法。所要实现的方法有:
258 |
259 | * **search_read(<title>)** 通过标题查找图书数据
260 | * **create(<title>)** 使用指定标题创建图书
261 | * **write(<id>, <title>)** 使用图书ID更新书名
262 | * **unlink(<id>)** 使用ID删除图书
263 |
264 | 在电脑上选择一个目录存放应用文件,并创建**library_xmlrpc.py**文件。先添加类的构造方法,如下:
265 |
266 | ```
267 | import xmlrpc.client
268 |
269 | class LibraryAPI:
270 | def __init__(self, host, port, db, user, pwd):
271 | common = xmlrpc.client.ServerProxy(
272 | "http://%s:%d/xmlrpc/2/common" % (host, port))
273 | self.api = xmlrpc.client.ServerProxy(
274 | "http://%s:%d/xmlrpc/2/object" % (host, port))
275 | self.uid = common.authenticate(db, user, pwd, {})
276 | self.pwd = pwd
277 | self.db = db
278 | self.model = "library.book"
279 | ```
280 |
281 | 类中存储了执行对目标模块调用所需的所有信息:API XML-RPC引用、**uid**、密码、数据库名以及模型名。
282 |
283 | 对Odoo的RPC调用会使用相同的**execute_kw** RPC方法。下面对其添加一层封装,放在**_execute()** 私有方法中。它利用对象存储的数据提供更小的函数签名,如以下代码所示:
284 |
285 | ```
286 | def _execute(self, method, arg_list, kwarg_dict=None):
287 | return self.api.execute_kw(
288 | self.db, self.uid, self.pwd, self.model,
289 | method, arg_list, kwarg_dict or {})
290 | ```
291 |
292 | **_execute()** 私有方法用于让更高阶的方法实现更简洁。
293 |
294 | 第一个公有方法是**search_read()**。它接收 一个可选字符串用于搜索书名。如未提供标题,则会返回所有记录。相应的实现如下:
295 |
296 | ```
297 | def search_read(self, title=None):
298 | domain = [("name", "ilike", title)] if title else []
299 | fields = ["id", "name"]
300 | return self._execute("search_read", [domain, fields])
301 | ```
302 |
303 | **create()** 方法用于按给定书名创建新书并返回所创建记录的 ID:
304 |
305 | ```
306 | def create(self, title):
307 | vals = {"name": title}
308 | return self._execute("create", [vals])
309 | ```
310 |
311 | **write()** 方法中传入新书名和图书 ID 作为参数,对该书执行写操作:
312 |
313 | ```
314 | def write(self, id, title):
315 | vals = {"name": title}
316 | return self._execute("write", [[id], vals])
317 | ```
318 |
319 | 最后**unlink()** 方法用于删除给定ID的图书:
320 |
321 | ```
322 | def unlink(self, id):
323 | return self._execute("unlink", [[id]])
324 | ```
325 |
326 | 在该Python文件最后添加一段测试代码在运行时执行:
327 |
328 | ```
329 | if __name__ == "__main__":
330 | # 测试配置
331 | host, port, db = "localhost", 8069, "odoo-dev"
332 | user, pwd = "admin", "admin"
333 | api = LibraryAPI(host, port, db, user, pwd)
334 | from pprint import pprint
335 |
336 | pprint(api.search_read())
337 | ```
338 |
339 | 如果执行以上 Python 脚本,我们可以打印出图书的内容:
340 |
341 | ```
342 | $ python3 library_xmlrpc.py
343 | [{'id': 3, 'name': 'Brave New World'},
344 | {'id': 2, 'name': 'Odoo 11 Development Cookbook'},
345 | {'id': 1, 'name': 'Odoo Development Essentials 11'}]
346 | ```
347 |
348 | 现在已经有了对 Odoo 后台的简单封装,下面就可以处理命令行用户接口了。
349 |
350 | ## 实现客户端用户接口
351 |
352 | 我的目标是学习如何写外部应用和 Odoo 服务之间的接口,前面已经实现了。但不能止步于此,我们再为这个最小化客户端应用构建一个用户接口。
353 |
354 | 为保持尽量简单,我们使用简单的命令行用户接口并避免使用其它依赖。那我们可以使用Python 内置功能和**ArgumentParser**库来实现这个命令行应用。代码如下:
355 |
356 | ```
357 | from argparse import ArgumentParser
358 | from library_xmlrpc import LibraryAPI
359 | ```
360 |
361 | 下面我们来看看参数解析器接收的命令,有以下四条命令:
362 |
363 | * **list** 搜索并列出图书
364 | * **add** 添加图书
365 | * **set** 修改书名
366 | * **del** 删除图书
367 |
368 | 实现以上命令的命令行解析代码如下:
369 |
370 | ```
371 | parser = ArgumentParser()
372 | parser.add_argument(
373 | "command",
374 | choices=["list", "add", "set", "del"])
375 | parser.add_argument("params", nargs="*") # 可选参数
376 | args = parser.parse_args()
377 | ```
378 |
379 | 这里的args对象表示用户传入的参数。**args.command**是所用到的命令,在给定**args.params**时,其存储的是命令所使用的其它参数。
380 |
381 | 如果未传参数或参数错误,参数解析器会进行处理,提示用户应该输入的内容。有关argparse更完整的说明,请参考[官方文档](https://docs.python.org/3/library/argparse.html)。
382 |
383 | 下一步是执行操作响应用户输入的命令。我们先创建一个**LibraryAPI**实例。这需要提供详细的Odoo连接信息,在我们的简单实现中采用了硬编码,代码如下:
384 |
385 | ```
386 | host, port, db = "localhost", 8069, "odoo-dev"
387 | user, pwd = "admin", "admin"
388 | api = LibraryAPI(host, port, db, user, pwd)
389 | ```
390 |
391 | 第一行代码设置服务实例的一些固定参数以及要连接的数据库。本例中,我们连接本地 Odoo 服务(localhost),监听**8069**默认端口,并使用 **odoo-dev**数据库。如需连接其它服务器和数据库,请对参数进行相应调整。
392 |
393 | 还需要添加代码处理每条命令。我们先从返回图书列表的**list**命令开始:
394 |
395 | ```
396 | if args.command == "list":
397 | title = args.params[:1]
398 | if len(title) != 0:
399 | title = title[0]
400 | books = api.search_read(title)
401 | for book in books:
402 | print("%(id)d %(name)s" % book)
403 | ```
404 |
405 | 这里我们使用了**LibraryAPI.search_read()** 来从服务端获取图书记录列表。然后遍历列表中每个元素并打印。
406 |
407 | 下面添加add命令:
408 |
409 | ```
410 | if args.command == "add":
411 | title = args.params[0]
412 | book_id = api.create(title)
413 | print("Book added with ID %d for title %s." % (book_id, title))
414 | ```
415 |
416 | 因为主要的工作已经在**LibraryAPI** 对象中完成,我们只要调用**create()** 方法并向终端用户显示结果即可。
417 |
418 | **set**命令允许我们修改已有图书的书名,应传入两个参数,新书名和图书的 ID:
419 |
420 | ```
421 | if args.command == "set":
422 | if len(args.params) != 2:
423 | print("set command requires a Title and ID.")
424 | else:
425 | book_id, title = int(args.params[0]), args.params[1]
426 | api.write(book_id, title)
427 | print("Title of Book ID %d set to %s." % (book_id, title))
428 | ```
429 |
430 | 最终我们要实现 **del** 命令来删除图书记录。实现方式和之前并没有什么差别:
431 |
432 | ```
433 | if args.command == "del":
434 | book_id = int(args.params[0])
435 | api.unlink(book_id)
436 | print("Book with ID %s was deleted." % book_id)
437 | ```
438 |
439 | 客户端应用至此已完成,可以尝试使用一些命令。应该可以执行本文开头的那些命令。
440 |
441 | > 小贴士:在Linux系统中,可通过执行**chmod +x library.py**命令并在文件的首行添加**#!/usr/bin/env python3**来让**library.py**文件变为可执行。之后就可以在命令行中运行了**./library.py**。
442 |
443 | 这是一个非常基础的应用,还有很多改进的方式。我们的目的是使用Odoo RPC API构建一个最小可用应用。
444 |
445 | 
446 |
447 | ## 使用OdooRPC库
448 |
449 | 另一个可以考虑的客户端库是**OdooRPC**。它是一个完整的客户端库,把XML-RPC协议换成了JSON-RPC 协议。事实上 Odoo 官方客户端使用的就是JSON-RPC,XML-RPC更多是用于支持向后兼容性。
450 |
451 | > ℹ️OdooRPC库现在由 OCA 管理和持续维护。了解更多请参见[OCA](https://github.com/OCA/odoorpc)。
452 |
453 | **OdooRPC**库可通过PyPI安装:
454 |
455 | ```
456 | pip3 install odoorpc
457 | ```
458 |
459 | **OdooRPC** 在新建 **odoorpc.ODOO** 对象时配置了服务端连接。此时我们应使用**ODOO.login()** 方法创建一个用户会话。和服务端一样,会员有一个包含会话环境的**env**属性,包括用户ID、**uid** 和上下文。
460 |
461 | **OdooRPC**库可用于对服务端的**library_xmlrpc.py**接口提供一个替代实现。功能相同,只是把XML-RPC换成了JSON-RPC。
462 |
463 | 创建**library_odoorpc.py** Python模块来对**library_xmlrpc.py**模块进行修改。新建的**library_odoorpc.py**文件中加入如下代码:
464 |
465 | ```
466 | import odoorpc
467 |
468 | class LibraryAPI():
469 | def __init__(self, host, port, db, user, pwd):
470 | self.api = odoorpc.ODOO(host, port=port)
471 | self.api.login(db, user, pwd)
472 | self.uid = self.api.env.uid
473 | self.model = "library.book"
474 | self.Model = self.api.env[self.model]
475 |
476 | def _execute(self, method, arg_list, kwarg_dict=None):
477 | return self.api.execute(
478 | self.model,
479 | method, *arg_list, **kwarg_dict)
480 | ```
481 |
482 | **OdooRPC**库实现**Model**和**Recordset**对象来模拟服务端对应的功能。目标是在客户端编程与服务端编程应基本一致。客户端使用的方法利用这点并在**self.Mode**l属性中存储对**library.book**模型引用,通过OdooRPC的**env["library.book"]**调用提供。
483 |
484 | 这里同样实现了**_execute()**方法,可与XML-RPC版本进行对比。OdooRPC库中的**execute()**方法可运行指定的Odoo模型方法。
485 |
486 | 下面我们来实现**search_read(**), **create()**, **write()**和**unlink()**这些客户端方法。在相同文件的**LibraryAPI()**类中添加如下方法:
487 |
488 | ```
489 | def search_read(self, title=None):
490 | domain = [("name", "ilike", title)] if title else []
491 | fields = ["id", "name"]
492 | return self.Model.search_read(domain, fields)
493 |
494 | def create(self, title):
495 | vals = {"name": title}
496 | return self.Model.create(vals)
497 |
498 | def write(self, id, title):
499 | vals = {"name": title}
500 | self.Model.write(id, vals)
501 |
502 | def unlink(self, id):
503 | return self.Model.unlink(id)
504 | ```
505 |
506 | 注意这段代码和 Odoo 服务端代码极其相似。
507 |
508 | 可使用**LibraryAPI**对象替换**library_xmlrpc.py**。可通过编辑**library.py**文件将**from library_xmlrpc import LibraryAPI**一行替换为**from library_odoorpc import LibraryAPI**将其用作RPC连接层。然后对**library.py**客户端应用进行测试,执行效果应该是和之前一样的。
509 |
510 | ## 了解ERPpeek客户端
511 |
512 | ERPpeek是一个多功能工具,既可以作为交互式命令行接口(CLI)也可以作为 Python库,它提供了比xmlrpc库更便捷的 API。它在PyPi索引中,可通过如下命令安装:
513 |
514 | ```
515 | pip3 install erppeek
516 | ```
517 |
518 | ERPpeek不仅可用作 Python 库,它还可作为 CLI 来在服务器上执行管理操作。Odoo shell 命令在主机上提供了一个本地交互式会话功能,而erppeek库则为网络上的客户端提供了一个远程交互式会话。打开命令行,通过以下命令可查看能够使用的选项:
519 |
520 | ```
521 | erppeek --help
522 | ```
523 |
524 | 下面一起来看看一个示例会话:
525 |
526 | ```
527 | $ erppeek --server='http://127.0.0.1:8069' -d odoo-dev -uadmin
528 | Usage (some commands):
529 | models(name) # List models matching pattern
530 | model(name) # Return a Model instance
531 | ...
532 |
533 | Password for 'admin':
534 | Logged in as 'admin'
535 | odoo-dev >>> model('res.users').count()
536 | 3
537 | odoo-dev >>> rec = model('res.partner').browse(14)
538 | odoo-dev >>> rec.name
539 | 'Azure Interior'
540 | ```
541 |
542 | 如上所见,建立了服务端的连接,执行上下文引用了model() 方法来获得模型实例并对其进行操作。连接使用的erppeek.Client实例也可通过客户端变量来使用。 值得一提的是它可替代网页客户端来管理所安装的插件模块:
543 |
544 | * client.modules()列出可用或已安装模块
545 | * client.install()执行模块安装
546 | * client.upgrade()执行模块升级
547 | * client.uninstall()卸载模块
548 |
549 | 因此ERPpeek可作为 Odoo 服务端远程管理的很好的服务。有关ERPpeek的更多细节请见 [GitHub](https://github.com/tinyerp/erppeek)。
550 |
551 | 
552 |
553 |
554 | ## 小结
555 |
556 | 本文的目标是学习外部 API 如何运作以及它们能做些什么。一开始我们通过一个简单的Python XML-RPC客户端来进行探讨,但外部 API 也可用于其它编程语言。事实上官方文档中包含了Java, PHP和Ruby的代码示例。
557 |
558 | 然后我们学习了如何使用XML-RPC调用搜索、读取数据,以及如何调用其它方法。比如我们可以创建、更新和删除记录。
559 |
560 | 接着我们介绍了OdooRPC库。它在RPC基础库(XML-RPC 或 JSON-RPC) 上提供了一层,用于提供类似服务端API的本地API。这降低了学习曲线,减少了编程失误并且让在服务端和客户端之间拷贝代码变得更容易。
561 |
562 | 以上我们就完结了本文有关编程 API 和业务逻辑的学习。是时候深入视图和用户界面了。在下一篇文章中,我们进一步学习后台视图以及web客户端提供的开箱即用的用户体验。
563 |
564 | ## 扩展阅读
565 |
566 | 以下参考资料可用于补充本文所学习的内容:
567 |
568 | * Odoo web服务的[官方文档](https://www.odoo.com/documentation/15.0/developer/api/external_api.html)中包含了Python以外编程语言的代码示例。
569 | * [OdooRPC文档](https://pythonhosted.org/OdooRPC/)
570 | * [ERPpeek文档](https://erppeek.readthedocs.io/en/latest/)
571 |
--------------------------------------------------------------------------------