来进行定义,其中 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/)
--------------------------------------------------------------------------------
/13.md:
--------------------------------------------------------------------------------
1 | # 第十三章 Odoo 12开发之创建网站前端功能
2 |
3 | 本文为[最好用的免费ERP系统Odoo 12开发手册](README.md)系列文章第十三篇。
4 |
5 | Odoo 起初是一个后台系统,但很快就有了前端界面的需求。早期基于后台界面的门户界面不够灵活并且对移动端不友好。为解决这一问题,Odoo 引入了新的网站功能,为系统添加了 CMS(Content Management System)内容管理系统。这使得我们无需集成第三方 CMS 便可创建美观又高效的前端。本文中我们将学习如何利用 Odoo 自带的网站功能开发面向前端的插件模块。
6 |
7 | 本文主要内容有:
8 |
9 | - 学习项目 - 自助图书馆
10 | - 第一个网页
11 | - 创建网站
12 |
13 |
14 |
15 | ## 开发准备
16 |
17 | 我将用第十一章 [Odoo 12开发之看板视图和用户端 QWeb](11.md)中最后编辑的library_checkout插件模块,代码请见[GitHub 仓库](source-code/chapter11)。本文完成后的代码也请参见[GitHub 仓库](source-code/chapter13)。
18 |
19 | ## 学习项目 - 自助图书馆
20 |
21 | 本文中我们将为图书会员添加一个自助服务功能。可供会员分别登录账号来访问他们的借阅请求列表。这样我们就可以学习网站开发的基本技术:创建动态页面、在页面间传递参数、创建表单以及处理表单数据验证。对这些新的图书网站功能,我们要新建一个插件模块library_website。
22 |
23 | 大家应该已经轻车熟路了,首先创建插件的声明文件ibrary_website/__manifest__.py,代码如下:
24 |
25 | ```
26 | {
27 | 'name': 'Library Website',
28 | 'description': 'Create and check book checkout requests.',
29 | 'author': 'Alan Hou',
30 | 'depends': [
31 | 'library_checkout'
32 | ],
33 | 'data': [
34 | 'security/ir.model.access.csv',
35 | 'security/library_security.xml',
36 | 'views/library_member.xml',
37 | ],
38 | }
39 | ```
40 |
41 | 网站功能将会依赖于library_checkout。我们并没有添加对website核心插件模块的依赖。website插件为创建完整功能的网站提供了有用的框架,但现在我们仅探讨核心框架自带的基础网站功能,尚无需使用website。我们想要图书会员通过登录信息在图书网站上访问自己的借阅请求。为此需要在图书会员模型中添加一个user_id字段,需要分别在模型和视图中添加,下面就开始进行网站的创建:
42 |
43 | 1、添加library_website/models/library_member.py文件
44 |
45 | ```
46 | from odoo import fields, models
47 |
48 | class Member(models.Model):
49 | _inherit = 'library.member'
50 | user_id = fields.Many2one('res.users')
51 | ```
52 |
53 | 2、添加library_website/models/__init__.py文件:
54 |
55 | ```
56 | from . import library_member
57 | ```
58 |
59 | 3、添加library_website/__init__.py文件:
60 |
61 | ```
62 | from . import models
63 | ```
64 |
65 | 4、添加library_website/views/library_member.xml文件:
66 |
67 | ```
68 |
69 |
70 |
71 | Member Form
72 | library.member
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | ```
82 |
83 | 访问这些网页的都是门户用户,无需访问后台菜单。我们需要为这个用户组设置安全访问权限,否则会在使用图书网站功能时报权限错误。
84 |
85 | 5、添加library_website/security/ir.model.access.csv文件,添加对图书模型的读权限:
86 |
87 | ```
88 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
89 | access_book_portal,Book Portal Access,library_app.model_library_book,base.group_
90 | portal,1,0,0,0
91 | access_member_portal,Member Portal Access,library_member.model_library_member,ba
92 | se.group_portal,1,0,0,0
93 | access_checkout_portal,Checkout Portal Access,library_checkout.model_library_che
94 | ckout,base.group_portal,1,0,0,0
95 | ```
96 |
97 | 6、在library_website/security/library_security.xml文件中添加记录规则来限制门户用户所能访问的记录:
98 |
99 | ```
100 |
101 |
102 |
103 |
104 | Library Member Portal Access
105 |
106 |
107 | [('user_id', '=', user.id)]
108 |
109 |
110 |
111 |
112 |
113 | Library Checkout Portal Access
114 |
115 |
116 | [('member_id.user_id', '=', user.id)]
117 |
118 |
119 |
120 |
121 |
122 | ```
123 |
124 | base.group_portal是门户用户组的标识符。在创建门户用户时,应设置他们的用户类型为 Portal,而不是Internal User。这会让他们属于门户用户组并继承我们上面定义的访问权限:
125 |
126 | 
127 |
128 | 补充:以上内容需开启开发者模式才可见
129 |
130 | 一旦为图书会员创建了一个门户用户,就应在我们会员表单中的用户字段中使用。该登录信息将可以访问相应会员的借阅请求。
131 |
132 | > **小贴士:**在模型中使用 ACL 和记录规则来实现安全权限比使用控制器的逻辑要更为安全。这是因为攻击者有可能跳过网页控制器直接使用RPC 来访问模型 API 。
133 |
134 | 了解了这些,我们就可以开始实现图书网站的功能了。但首先我们来使用简单的Hello World网页简短地介绍下基本网站概念。
135 |
136 | ## 第一个网页
137 |
138 | 要开始了解 Odoo 网页开发的基础,我们将先实现一个Hello World网页来展示基本概念和技术。很有想象空间,是不是?
139 |
140 | 要创建第一个网页,我们需要一个控制器对象。首先来添加controllers/hello.py文件:
141 |
142 | 1、在library_website/__init__.py文件中添加如下行:
143 |
144 | ```
145 | from . import controllers
146 | ```
147 |
148 | 2、在library_website/controllers/__init__.py文件中添加如下行:
149 |
150 | ```
151 | from . import hello
152 | ```
153 |
154 | 3、添加实际的控制器文件 library_website/controllers/hello.py,代码如下:
155 |
156 | ```
157 | from odoo import http
158 |
159 | class Hello(http.Controller):
160 | @http.route('/helloworld', auth="public")
161 | def helloworld(self):
162 | return('Hello World!
')
163 | ```
164 |
165 | odoo.http模块提供 Odoo 网页相关的功能。我们用于渲染页面的控制器,应该是一个继承了odoo.http.Controller类的对象。实际使用的名称并不是太重要,这里选择了 Hello(),一个常用的选择是 Main()。
166 |
167 | 在控制器类中使用了匹配 URL 路由的方法。这些路由用于做一些处理并返回结果,通常是返回用户网页浏览器的 HTML 页面。odoo.http.route装饰器用于为 URL 路由绑定方法,本例中使用的是/helloworld 路由。
168 |
169 | 安装library_website模块(~/odoo-dev/odoo/odoo-bin -d dev12 -i library_website)就可以在浏览器中打开http://xxx:8069/helloworld,我们应该就可以看到Hello World问候语了。
170 |
171 | 本例中方法执行的处理非常简单,它返回一个带有 HTML 标记的文本字符串,Hello World。
172 |
173 | > ℹ️使用这里的简单 URL 访问按制器,如果同一 Odoo 实例有多个数据库时,在没有指定目标数据库的情况下将会失败。这可通过在启动配置中设置-d或--db-filter来解决,参见第二章 [Odoo 12开发之开发环境准备](2.md)。
174 |
175 | 你可能注意到在路由装饰中使用了auth='public'参数,对于无需登录的用户开放的页面就需要使用它。如果删除该参数,仅有登录用户方可浏览此页面。如果没有活跃会话(session)则会进入登录页面。
176 |
177 | > **小贴士:**auth='public'参数实际表示如果访客未登录则使用public特殊用户运行网页控制器。如果登录了,则使用登录用户来代替public。
178 |
179 | 
180 |
181 | ### 使用 QWeb 模板的 Hello World
182 |
183 | 使用 Python 字符串来创建 HTML 很快就会觉得乏味。QWeb可用来增添色彩,下面就使用模板来写一个改进版的Hello World网页。QWeb模板通过 XML 数据文件添加,技术层面上它是与表单、列表视图类似的一种视图类型。它们甚至存储在同一个技术模型ir.ui.view中。
184 |
185 | 老规矩,需要在声明文件中添加声明来加载文件,编辑library_website/__manifest__.py文件并添加内容如下:
186 |
187 | ```
188 | 'data': [
189 | ...
190 | 'views/helloworld_template.xml',
191 | ],
192 | ```
193 |
194 | 然后添加实际的数据文件views/helloworld_template.xml,内容如下:
195 |
196 | ```
197 |
198 |
199 |
200 | Hello again World!
201 |
202 |
203 | ```
204 |
205 | 实际上是一种简写形式,它声明将数据以type="qweb"类型加载到ir.ui.view模型中。现在,我们需要修改控制器方法来使用这个模板:
206 |
207 | ```
208 | from odoo import http
209 | from odoo.http import request
210 |
211 | class Hello(http.Controller):
212 |
213 | @http.route('/helloworld', auth="public")
214 | def helloworld(self, **kwargs):
215 | return request.render('library_website.helloworld')
216 | ```
217 |
218 | 模板的渲染是通过render()函数的 request 对象来实现的。
219 |
220 | > **小贴士:**注意我们添加了**kwargs方法参数。使用该参数,HTTP 请求中的任意附加参数,如GET 或 POST 请求参数,可通过 kwargs 字典捕获。这会让我们的方法更加健壮,因为即便添加了未预期的参数也不会产生错误。
221 |
222 | 
223 |
224 | ### HelloCMS!
225 |
226 | 下面我们来增加点趣味性,创建我们自己的简单 CMS。为此我们可以通过 URL在路由中使用模板名(一个页面),然后对其进行渲染。然后就可以动态创建网页,通过我们的 CMS 来提供服务。实现方法很简单:
227 |
228 | ```
229 | @http.route('/hellocms/', auth='public')
230 | def hello(self, page, **kwargs):
231 | return http.request.render(page)
232 | ```
233 |
234 | 以上page 参数应匹配一个模板的外部ID,如果在浏览器中打开http://xxx:8069/hellocms/library_website.helloworld,应该又可以看到熟悉的Hello World 页面了。实际上内置的website模块提供了CMS功能,在 /page路径(endpoint)下还包含更为健壮的实现。
235 |
236 | 
237 |
238 | > ℹ️在werkzeug的行话中,endpoint是路由的别名,由其静态部分(不含占位符)来表示。比如,CMS 示例中的 endpoint为/hellocms。
239 |
240 | 大多数情况下,我们要将页面集成到 Odoo 网站中,因此接下来的示例将使用website插件模块。
241 |
242 | ## 创建网站
243 |
244 | 前面的示例并未集成到 Odoo 网站中,并有页面 footer 和网站菜单。Odoo 的website插件模板为方便大家提供这些功能。
245 |
246 | 要使用网站功能,我们需要在工作实例中安装website插件模块。应当在library_website插件模块中添加这一依赖,修改__manifest__.py的 depends 内容如下:
247 |
248 | ```
249 | 'depends': [
250 | 'library_checkout',
251 | 'website',
252 | ],
253 | ```
254 |
255 | 要使用网站功能,我们需要对控制器和 QWeb模板进行一些修改。控制器中可在路由上添加一个额外的website=True参数:
256 |
257 | ```
258 | @http.route('/helloworld', auth="public", website=True)
259 | def helloworld(self, **kwargs):
260 | return request.render('library_website.helloworld')
261 | ```
262 |
263 | 集成website模块并非严格要求website=True参数,不添加它也可以在模板视图中添加网站布局。但是通过添加可以让我们在网页控制器中使用一些功能:
264 |
265 | - 路由会自动变成支持多语言并且会从网站安装的语言中自动检测最接近的语言。需要说明这可能会导致重新路由和重定向。
266 | - 控制器抛出的任何异常都会由网站代码进行处理,这会将默认的错误码变成更友好的错误页面向访客展示。
267 | - 带有当前网站浏览记录的request.website变量,可在请求中进行使用。
268 | - auth=public路由的 public用户将是由后台网站配置中选择的用户。这可能会和本地区、时区等相关。
269 |
270 | 如果在网页控制器中无需使用上述功能,则可省略website=True参数。但大多数网站QWeb模板需要使用website=True开启一些数据,比如底部公司信息,所以最好还是添加上。
271 |
272 | > ℹ️传入QWeb运行上下文语言的网站数据由website/model/ir_ui_view.py文件中的_prepare_qcontext方法设定。
273 |
274 | 要在模板中添加网站的基本布局,应为QWeb/HTML包裹一个t-call="website.layout"指令,如下所示:
275 |
276 | ```
277 |
278 |
279 | Hello World!
280 |
281 |
282 | ```
283 |
284 | t-call运行QWeb模板website.layout并向其传递 XML 内的tcall 节点。website.layout设计用于渲染带有菜单、头部和底部的完整网页,交将传入的内容放在对应的主区域内。这样,我们的Hello World!示例内容就会显示在 Odoo 网站页面中了。
285 |
286 | 
287 |
288 | ### 添加 CSS 和 JavaScript 资源
289 |
290 | 我们的网站页面可能需要一些其它的 CSS 或JavaScript资源。这方面的网页由website 管理,因此需要一个方式来告诉它使用这些文件。我们将使用 CSS 来添加一个简单的删除线效果,创建library_website/static/src/css/library.css文件并添加如下内容:
291 |
292 | ```
293 | .text-strikeout {
294 | text-decoration: line-through;
295 | }
296 | ```
297 |
298 | 接下来需要在网站页面中包含该文件。通过在website.assets_frontend模板中添加来实现,该模板用于加载网站相关的资源。添加library_website/views/website_assets.xml数据文件来继承该模板:
299 |
300 | ```
301 |
302 |
303 |
306 |
307 |
309 |
310 |
311 |
312 | ```
313 |
314 | 很快我们就会使用text-strikeout这个新的样式类。当然,可以使用相似的方法来添加JavaScript资源。
315 |
316 | ### 借阅列表控制器
317 |
318 | 既然我们已经过了一遍基础知识,就来一起实现借阅列表吧。我们需要使用/checkout URL来显示借阅列表的网页。为此我们需要一个控制器方法来准备要展示的数据,以及一个QWeb模板来向用户进行展示。
319 |
320 | 在模块中添加library_website/controllers/main.py文件,代码如下:
321 |
322 | ```
323 | from odoo import http
324 | from odoo.http import request
325 |
326 | class Main(http.Controller):
327 | @http.route('/checkouts', auth='user', website=True)
328 | def checkouts(self, **kwargs):
329 | Checkout = request.env['library.checkout']
330 | checkouts = Checkout.search([])
331 | return request.render(
332 | 'library_website.index',
333 | {'docs': checkouts})
334 | ```
335 |
336 | 控制器获取要使用的数据并传给渲染的模板。本例中控制器需要一个登录了的会话,因为路由中有一个auth='user'属性。这是默认行为,推荐明确指出需要用户会话。登录了的用户存储在环境对象中,通过 request.env来使用。search()语句使用它来过滤出相应的借阅记录。
337 |
338 | 对于无需登录即可访问的控制器,所能读取的数据也是非常有限的。这种情况下,我们经常需要对部分代码采用提权上下文运行。这时我们可使用sudo()模型方法,它将权限上下文权限修改为内部超级用户,突破大部分限制。权力越大,责任越大,我们要小心这种操作带来的安全风险。需要特别注意在提权时输入的参数以及执行的操作的有效性。建议将sudo() 记录集操作控制在最小范围内。
339 |
340 | 回到我们的代码,它以request.render()方法收尾。和之前一样,我们传入了QWeb模板渲染的标识符,和模板运行用到的上下文字典。本例中我们向模板传入 docs 变量,该变量包含要渲染借阅记录的记录集。
341 |
342 | ### 借阅 QWeb 模板
343 |
344 | QWeb模板使用数据文件来添加,我们可以使用library_website/views/checkout_template.xml文件并添加如下代码:
345 |
346 | ```
347 |
348 |
349 |
350 |
351 |
352 |
Checkouts
353 |
354 |
355 |
356 |
364 |
365 |
366 |
367 |
368 |
369 | ```
370 |
371 | 以上代码使用t-foreach指令来迭代 docs 记录集。我们使用了复选框 input 并在借阅完成时保持为已选状态。在 HTML 中,复选框是否被勾选取决于是否有 checked 属性。为此我们使用了t-att-NAME指定来根据表达式动态渲染 checked 属性。当表达式运行结果为 None(或任意其它 false 值)时,QWeb会忽略该属性,本例用它就非常方便了。
372 |
373 | 在渲染任务名时,t-attf指令用于动态创建打开每个指定任务的明细表单的URL。我们使用一个特殊函数slug()来为每条记录生成易于阅读的 URL。该链接目前尚无法使用,因为我们还没有创建对应的控制器。
374 |
375 | 在每条借阅记录上,我们还使用了t-att 指令来在借阅为最终状态时应用text-strikeout样式。
376 |
377 | 
378 |
379 | ### 借阅明细页面
380 |
381 | 借阅列表中的每一项都有一个相应明细页面的链接。我们就为这些链接实现一个控制器,以及实现一个QWeb模板来用于展示。说到这里应该已经很明朗了。
382 |
383 | 在library_website/controllers/main.py文件中添加如下方法:
384 |
385 | ```
386 | class Main(http.Controller):
387 | ...
388 |
389 | @http.route('/checkout/',
390 | auth='user', # 默认值,但此处明确指定
391 | website=True)
392 | def checkout(self, doc, **kwargs):
393 | return http.request.render(
394 | 'library_website.checkout',
395 | {'doc': doc})
396 | ```
397 |
398 | 注意这里路由使用了带有model("library.checkout")转换器的占位符,会映射到方法的 doc 变量中。它从 URL 中捕获借阅标识符,可以是简单的 ID 数值或链接别名,然后转换成相应的浏览记录对象。
399 |
400 | 对于QWeb模板,应在library_website/views/checkout_template.xml数据文件中添加如下代码:
401 |
402 | ```
403 |
404 |
405 |
406 |
407 | Member:
408 | Stage:
409 |
410 |
411 |
412 | ```
413 |
414 | 这里值得一提的是使用了元素。和在后台中一样,它处理字段值的相应展示。比如,它正确地展示日期值和many-to-one值。
415 |
416 | 
417 |
418 | 补充:controllers/__init__.py和__mainfest__.py 中请自行添加控制器文件和数据文件的引用
419 |
420 | ## 总结
421 |
422 | 读者现在应该对网站功能的基础有了不错的掌握。我们学习了如何使用网页控制器和QWeb模板来动态渲染网页。然后学习了如何使用website插件并使用它来创建我们自己页面。最后,我们介绍了网站表单插件来帮助我们来创建网页表单。这些都是创建网站功能的核心能技巧。
423 |
424 | 我们已经学习了Odoo 主要构件的开发,是时候学习如何将Odoo 服务部署到生产环境了。
425 |
426 |
427 |
428 | ☞☞☞第十四章 [Odoo 12开发之部署和维护生产实例](14.md)
429 |
430 |
431 |
432 | ## 扩展阅读
433 |
434 | Odoo 官方文档中有一些对本文讲解课题的补充参考材料:
435 |
436 | - [网页控制器](https://www.odoo.com/documentation/12.0/reference/http.html)
437 | - [QWeb语言](https://www.odoo.com/documentation/12.0/reference/qweb.html)
438 | - [JavaScript API指南](https://www.odoo.com/documentation/12.0/reference/javascript_reference.html)
439 | - [Bootstrap样式文档](https://getbootstrap.com/docs/4.1/getting-started/introduction/)
440 | - 还可以在[Packt](https://www.packtpub.com/tech/Bootstrap)上找到更多的 Bootstrap学习资源
441 |
442 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Odoo开发手册
2 |
3 |
4 |
5 | ## Odoo 15开发手册目录
6 |
7 | * [第一章 使用开发者模式快速入门 Odoo 15](1.md)
8 | * [第二章 Odoo 15开发之开发环境准备](2.md)
9 | * [第三章 Odoo 15开发之创建第一个 Odoo 应用](3.md)
10 | * [第四章 Odoo 15开发之模块继承](4.md)
11 | * [第五章 Odoo 15开发之导入、导出以及模块数据](5.md)
12 | * [第六章 Odoo 15开发之模型 - 结构化应用数据](6.md)
13 | * [第七章 Odoo 15开发之记录集 - 使用模型数据](7.md)
14 | * [第八章 Odoo 15开发之业务逻辑 - 业务流程的支持](8.md)
15 | * [第九章 Odoo 15开发之外部 API - 集成第三方系统](9.md)
16 |
17 | 代码地址:[Source Code](./source-code/)
18 |
19 | ## Odoo 12开发手册目录
20 |
21 | * 第一章 [使用开发者模式快速入门 Odoo 12](https://github.com/iTranslateX/odoo-essentials/tree/v12/1.md)
22 |
23 | * 第二章 [Odoo 12开发之开发环境准备](https://github.com/iTranslateX/odoo-essentials/tree/v12/2.md)
24 |
25 | * 第三章 [Odoo 12 开发之创建第一个 Odoo 应用](https://github.com/iTranslateX/odoo-essentials/tree/v12/3.md)
26 |
27 | * 第四章 [Odoo 12 开发之模块继承](https://github.com/iTranslateX/odoo-essentials/tree/v12/4.md)
28 |
29 | * 第五章 [Odoo 12开发之导入、导出以及模块数据](https://github.com/iTranslateX/odoo-essentials/tree/v12/5.md)
30 |
31 | * 第六章 [Odoo 12开发之模型 - 结构化应用数据](https://github.com/iTranslateX/odoo-essentials/tree/v12/6.md)
32 |
33 | * 第七章 [Odoo 12开发之记录集 - 使用模型数据](https://github.com/iTranslateX/odoo-essentials/tree/v12/7.md)
34 |
35 | * 第八章 [Odoo 12开发之业务逻辑 - 业务流程的支持](https://github.com/iTranslateX/odoo-essentials/tree/v12/8.md)
36 |
37 | * 第九章 [Odoo 12开发之外部 API - 集成第三方系统](https://github.com/iTranslateX/odoo-essentials/tree/v12/9.md)
38 |
39 | * 第十章 [Odoo 12开发之后台视图 - 设计用户界面](https://github.com/iTranslateX/odoo-essentials/tree/v12/10.md)
40 |
41 | * 第十一章 [Odoo 12开发之看板视图和用户端 QWeb](https://github.com/iTranslateX/odoo-essentials/tree/v12/11.md)
42 |
43 | * 第十二章 [Odoo 12开发之报表和服务端 QWeb](https://github.com/iTranslateX/odoo-essentials/tree/v12/12.md)
44 |
45 | * 第十三章 [Odoo 12开发之创建网站前端功能](https://github.com/iTranslateX/odoo-essentials/tree/v12/13.md)
46 |
47 | * 第十四章 [Odoo 12开发之部署和维护生产实例](https://github.com/iTranslateX/odoo-essentials/tree/v12/14.md)
48 |
49 | 代码地址:[Source Code](https://github.com/iTranslateX/odoo-essentials/tree/v12/source-code)
50 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import controllers
4 | from . import models
5 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | {
4 | "name": "Library Management",
5 | "summary": "Manage library catalog and book lending.",
6 | "author": "Alan Hou",
7 | "license": "AGPL-3",
8 | "category": "Services/Library",
9 | "website": "https://alanhou.org",
10 | "version": "15.0.1.0.0",
11 | "depends": ["base"],
12 | "data": [
13 | "security/library_security.xml",
14 | "security/ir.model.access.csv",
15 | "views/book_view.xml",
16 | "views/library_menu.xml",
17 | "views/book_list_template.xml",
18 | ],
19 | "application": True,
20 | }
21 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import main
--------------------------------------------------------------------------------
/source-code/ch03/library_app/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 |
3 |
4 | class Books(http.Controller):
5 | @http.route("/library/books")
6 | def list(self, **kwargs):
7 | Book = http.request.env['library.book']
8 | books = Book.search([])
9 | return http.request.render(
10 | "library_app.book_list_template",
11 | {"books": books}
12 | )
--------------------------------------------------------------------------------
/source-code/ch03/library_app/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import library_book
4 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 | from odoo.exceptions import ValidationError
3 |
4 |
5 | class Book(models.Model):
6 | _name = 'library.book'
7 | _description = 'Book'
8 | name = fields.Char("Title", required=True)
9 | isbn = fields.Char('ISBN')
10 | active = fields.Boolean('Active?', default=True)
11 | date_published = fields.Date()
12 | image = fields.Binary('Cover')
13 | publisher_id = fields.Many2one('res.partner', string='Publisher')
14 | author_ids = fields.Many2many('res.partner', string='Authors')
15 |
16 | def _check_isbn(self):
17 | self.ensure_one()
18 | isbn = self.isbn.replace('-', '')
19 | digits = [int(x) for x in isbn if x.isdigit()]
20 | if len(digits) == 13:
21 | ponderations = [1, 3] * 6
22 | terms = [a * b for a, b in zip(digits[:12], ponderations)]
23 | remain = sum(terms) % 10
24 | check = 10 - remain if remain !=0 else 0
25 | return digits[-1] == check
26 |
27 | def button_check_isbn(self):
28 | for book in self:
29 | if not book.isbn:
30 | raise ValidationError("Please provide an ISBN for %s" % book.name)
31 | if book.isbn and not book._check_isbn():
32 | raise ValidationError("%s ISBN is invalid" % book.isbn)
33 | return True
--------------------------------------------------------------------------------
/source-code/ch03/library_app/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_book_user,BookUser,model_library_book,library_group_user,1,1,1,0
3 | access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
4 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User
6 |
7 |
9 |
10 |
11 |
12 | Manager
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | Library Book User Access
23 |
24 |
25 | [('active', '=', True)]
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/static/description/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch03/library_app/static/description/icon.png
--------------------------------------------------------------------------------
/source-code/ch03/library_app/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_book
--------------------------------------------------------------------------------
/source-code/ch03/library_app/tests/test_book.py:
--------------------------------------------------------------------------------
1 | from odoo.tests.common import TransactionCase
2 |
3 |
4 | class TestBook(TransactionCase):
5 | def setUp(self, *args, **kwargs):
6 | super().setUp(*args, **kwargs)
7 | user_admin = self.env.ref("base.user_admin")
8 | self.env = self.env(user=user_admin)
9 | self.Book = self.env['library.book']
10 | self.book1 = self.Book.create({
11 | "name": "Odoo Development Essentials",
12 | "isbn": "879-1-78439-279-6"
13 | })
14 |
15 | def test_book_create(self):
16 | "New Books are active by default"
17 | self.assertEqual(self.book1.active, True)
18 |
19 | def test_check_isbn(self):
20 | "Check valid ISBN"
21 | self.assertTrue(self.book1._check_isbn)
--------------------------------------------------------------------------------
/source-code/ch03/library_app/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Books
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Book Form
5 | library.book
6 |
7 |
27 |
28 |
29 |
30 |
31 | Book List
32 | library.book
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Book Filters
45 | library.book
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/source-code/ch03/library_app/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Library Books
7 | library.book
8 | tree,form
9 |
10 |
11 |
15 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import controllers
4 | from . import models
5 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | {
4 | "name": "Library Management",
5 | "summary": "Manage library catalog and book lending.",
6 | "author": "Alan Hou",
7 | "license": "AGPL-3",
8 | "category": "Services/Library",
9 | "website": "https://alanhou.org",
10 | "version": "15.0.1.0.0",
11 | "depends": ["base"],
12 | "data": [
13 | "security/library_security.xml",
14 | "security/ir.model.access.csv",
15 | "views/book_view.xml",
16 | "views/library_menu.xml",
17 | "views/book_list_template.xml",
18 | ],
19 | "application": True,
20 | }
21 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import main
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/__pycache__/controllers.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/controllers/__pycache__/controllers.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/controllers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # from odoo import http
3 |
4 |
5 | # class LibraryApp(http.Controller):
6 | # @http.route('/library_app/library_app', auth='public')
7 | # def index(self, **kw):
8 | # return "Hello, world"
9 |
10 | # @http.route('/library_app/library_app/objects', auth='public')
11 | # def list(self, **kw):
12 | # return http.request.render('library_app.listing', {
13 | # 'root': '/library_app/library_app',
14 | # 'objects': http.request.env['library_app.library_app'].search([]),
15 | # })
16 |
17 | # @http.route('/library_app/library_app/objects/', auth='public')
18 | # def object(self, obj, **kw):
19 | # return http.request.render('library_app.object', {
20 | # 'object': obj
21 | # })
22 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 |
3 |
4 | class Books(http.Controller):
5 | @http.route("/library/books")
6 | def list(self, **kwargs):
7 | Book = http.request.env['library.book']
8 | books = Book.search([])
9 | return http.request.render(
10 | "library_app.book_list_template",
11 | {"books": books}
12 | )
--------------------------------------------------------------------------------
/source-code/ch04/library_app/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import library_book
4 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/models/__pycache__/models.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/models/__pycache__/models.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_app/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 | from odoo.exceptions import ValidationError
3 |
4 |
5 | class Book(models.Model):
6 | _name = 'library.book'
7 | _description = 'Book'
8 | name = fields.Char("Title", required=True)
9 | isbn = fields.Char('ISBN')
10 | active = fields.Boolean('Active?', default=True)
11 | date_published = fields.Date()
12 | image = fields.Binary('Cover')
13 | publisher_id = fields.Many2one('res.partner', string='Publisher')
14 | author_ids = fields.Many2many('res.partner', string='Authors')
15 |
16 | def _check_isbn(self):
17 | self.ensure_one()
18 | isbn = self.isbn.replace('-', '')
19 | digits = [int(x) for x in isbn if x.isdigit()]
20 | if len(digits) == 13:
21 | ponderations = [1, 3] * 6
22 | terms = [a * b for a, b in zip(digits[:12], ponderations)]
23 | remain = sum(terms) % 10
24 | check = 10 - remain if remain !=0 else 0
25 | return digits[-1] == check
26 |
27 | def button_check_isbn(self):
28 | for book in self:
29 | if not book.isbn:
30 | raise ValidationError("Please provide an ISBN for %s" % book.name)
31 | if book.isbn and not book._check_isbn():
32 | raise ValidationError("%s ISBN is invalid" % book.isbn)
33 | return True
--------------------------------------------------------------------------------
/source-code/ch04/library_app/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_book_user,BookUser,model_library_book,library_group_user,1,1,1,0
3 | access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
4 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User
6 |
7 |
9 |
10 |
11 |
12 | Manager
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | Library Book User Access
23 |
24 |
25 | [('active', '=', True)]
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/static/description/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_app/static/description/icon.png
--------------------------------------------------------------------------------
/source-code/ch04/library_app/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_book
--------------------------------------------------------------------------------
/source-code/ch04/library_app/tests/test_book.py:
--------------------------------------------------------------------------------
1 | from odoo.tests.common import TransactionCase
2 |
3 |
4 | class TestBook(TransactionCase):
5 | def setUp(self, *args, **kwargs):
6 | super().setUp(*args, **kwargs)
7 | user_admin = self.env.ref("base.user_admin")
8 | self.env = self.env(user=user_admin)
9 | self.Book = self.env['library.book']
10 | self.book1 = self.Book.create({
11 | "name": "Odoo Development Essentials",
12 | "isbn": "879-1-78439-279-6"
13 | })
14 |
15 | def test_book_create(self):
16 | "New Books are active by default"
17 | self.assertEqual(self.book1.active, True)
18 |
19 | def test_check_isbn(self):
20 | "Check valid ISBN"
21 | self.assertTrue(self.book1._check_isbn)
--------------------------------------------------------------------------------
/source-code/ch04/library_app/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Books
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Book Form
5 | library.book
6 |
7 |
27 |
28 |
29 |
30 |
31 | Book List
32 | library.book
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Book Filters
45 | library.book
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Library Books
7 | library.book
8 | tree,form
9 |
10 |
11 |
15 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/views/templates.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
--------------------------------------------------------------------------------
/source-code/ch04/library_app/views/views.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
26 |
27 |
28 |
42 |
43 |
44 |
47 |
48 |
52 |
53 |
59 |
60 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/__init__.py:
--------------------------------------------------------------------------------
1 | from . import models
2 | from . import controllers
3 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/__manifest__.py:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Library Members",
3 | "license": "AGPL-3",
4 | "description": "Manage people who will be able to borrow books.",
5 | "author": "Alan Hou",
6 | "depends": ["library_app", "mail"],
7 | "application": False,
8 | "data": [
9 | "security/library_security.xml",
10 | "security/ir.model.access.csv",
11 | "views/book_view.xml",
12 | "views/member_view.xml",
13 | "views/library_menu.xml",
14 | "views/book_list_template.xml",
15 | ]
16 | }
--------------------------------------------------------------------------------
/source-code/ch04/library_member/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import main
2 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 | from odoo.addons.library_app.controllers.main import Books
3 |
4 |
5 | class BookExtended(Books):
6 | @http.route()
7 | def list(self, **kwargs):
8 | response = super().list(**kwargs)
9 | if kwargs.get("available"):
10 | all_books = response.qcontext["books"]
11 | available_books = all_books.filtered("is_available")
12 | response.qcontext["books"] = available_books
13 | return response
14 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/__init__.py:
--------------------------------------------------------------------------------
1 | from . import library_book
2 | from . import library_member
3 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/__pycache__/library_member.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch04/library_member/models/__pycache__/library_member.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Book(models.Model):
5 | _inherit = 'library.book'
6 | is_available = fields.Boolean('Is Available?')
7 | isbn = fields.Char(help="Use a valid ISBN-13 or ISBN-10.")
8 | publisher_id = fields.Many2one(index=True)
9 |
10 | def _check_isbn(self):
11 | self.ensure_one()
12 | isbn = self.isbn.replace('-', '')
13 | digits = [int(x) for x in isbn if x.isdigit()]
14 | if len(digits) == 10:
15 | ponderators = [1, 2, 3, 4, 5, 6, 7, 8, 9]
16 | total = sum(a * b for a, b in zip(digits[:9], ponderators))
17 | check = total % 11
18 | return digits[-1] == check
19 | else:
20 | return super()._check_isbn()
21 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/models/library_member.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Member(models.Model):
5 | _name = 'library.member'
6 | _description = 'Library Member'
7 | _inherit = ["mail.thread", "mail.activity.mixin"]
8 | # _inherits = {"res.partner": "partner_id"}
9 | card_number = fields.Char()
10 | partner_id = fields.Many2one(
11 | "res.partner",
12 | delegate=True,
13 | ondelete="cascade",
14 | required=True)
--------------------------------------------------------------------------------
/source-code/ch04/library_member/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_member_user,Member User Access,model_library_member,library_app.library_group_user,1,1,1,0
3 | access_member_manager,Member Manager Access,model_library_member,library_app.library_group_manager,1,1,1,1
--------------------------------------------------------------------------------
/source-code/ch04/library_member/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Librarian
5 |
6 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | (Not Available)
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Book: add Is Available? field
4 | library.book
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
--------------------------------------------------------------------------------
/source-code/ch04/library_member/views/member_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Library Member Form View
4 | library.member
5 |
6 |
19 |
20 |
21 |
22 | Library Member List View
23 | library.member
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import controllers
4 | from . import models
5 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | {
4 | "name": "Library Management",
5 | "summary": "Manage library catalog and book lending.",
6 | "author": "Alan Hou",
7 | "license": "AGPL-3",
8 | "category": "Services/Library",
9 | "website": "https://alanhou.org",
10 | "version": "15.0.1.0.0",
11 | "depends": ["base"],
12 | "data": [
13 | "security/library_security.xml",
14 | "security/ir.model.access.csv",
15 | "views/book_view.xml",
16 | "views/library_menu.xml",
17 | "views/book_list_template.xml",
18 | ],
19 | "demo": [
20 | "data/res.partner.csv",
21 | "data/library.book.csv",
22 | "data/book_demo.xml",
23 | ],
24 | "application": True,
25 | }
26 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import main
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/__pycache__/controllers.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/controllers/__pycache__/controllers.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/controllers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # from odoo import http
3 |
4 |
5 | # class LibraryApp(http.Controller):
6 | # @http.route('/library_app/library_app', auth='public')
7 | # def index(self, **kw):
8 | # return "Hello, world"
9 |
10 | # @http.route('/library_app/library_app/objects', auth='public')
11 | # def list(self, **kw):
12 | # return http.request.render('library_app.listing', {
13 | # 'root': '/library_app/library_app',
14 | # 'objects': http.request.env['library_app.library_app'].search([]),
15 | # })
16 |
17 | # @http.route('/library_app/library_app/objects/', auth='public')
18 | # def object(self, obj, **kw):
19 | # return http.request.render('library_app.object', {
20 | # 'object': obj
21 | # })
22 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 |
3 |
4 | class Books(http.Controller):
5 | @http.route("/library/books")
6 | def list(self, **kwargs):
7 | Book = http.request.env['library.book']
8 | books = Book.search([])
9 | return http.request.render(
10 | "library_app.book_list_template",
11 | {"books": books}
12 | )
--------------------------------------------------------------------------------
/source-code/ch05/library_app/data/ library.book.csv:
--------------------------------------------------------------------------------
1 | "id","name","date_published","publisher_id/id","author_ids/id"
2 | library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
3 | library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"
--------------------------------------------------------------------------------
/source-code/ch05/library_app/data/book_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aldous Huxley
6 |
7 |
8 | Brave New World
9 |
11 | 1932-01-01
12 |
13 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/data/library.book.csv:
--------------------------------------------------------------------------------
1 | "id","name","date_published","publisher_id/id","author_ids/id"
2 | library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
3 | library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"
--------------------------------------------------------------------------------
/source-code/ch05/library_app/data/res.partner.csv:
--------------------------------------------------------------------------------
1 | id,name
2 | res_partner_alexandre,"Alexandre Fayolle"
3 | res_partner_daniel,"Daniel Reis"
4 | res_partner_holger,"Holger Brunn"
5 | res_partner_packt,"Packt Publishing"
--------------------------------------------------------------------------------
/source-code/ch05/library_app/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import library_book
4 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/models/__pycache__/models.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/models/__pycache__/models.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_app/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 | from odoo.exceptions import ValidationError
3 |
4 |
5 | class Book(models.Model):
6 | _name = 'library.book'
7 | _description = 'Book'
8 | name = fields.Char("Title", required=True)
9 | isbn = fields.Char('ISBN')
10 | active = fields.Boolean('Active?', default=True)
11 | date_published = fields.Date()
12 | image = fields.Binary('Cover')
13 | publisher_id = fields.Many2one('res.partner', string='Publisher')
14 | author_ids = fields.Many2many('res.partner', string='Authors')
15 |
16 | def _check_isbn(self):
17 | self.ensure_one()
18 | isbn = self.isbn.replace('-', '')
19 | digits = [int(x) for x in isbn if x.isdigit()]
20 | if len(digits) == 13:
21 | ponderations = [1, 3] * 6
22 | terms = [a * b for a, b in zip(digits[:12], ponderations)]
23 | remain = sum(terms) % 10
24 | check = 10 - remain if remain !=0 else 0
25 | return digits[-1] == check
26 |
27 | def button_check_isbn(self):
28 | for book in self:
29 | if not book.isbn:
30 | raise ValidationError("Please provide an ISBN for %s" % book.name)
31 | if book.isbn and not book._check_isbn():
32 | raise ValidationError("%s ISBN is invalid" % book.isbn)
33 | return True
--------------------------------------------------------------------------------
/source-code/ch05/library_app/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_book_user,BookUser,model_library_book,library_group_user,1,1,1,0
3 | access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
4 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User
6 |
7 |
9 |
10 |
11 |
12 | Manager
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | Library Book User Access
23 |
24 |
25 | [('active', '=', True)]
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/static/description/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_app/static/description/icon.png
--------------------------------------------------------------------------------
/source-code/ch05/library_app/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_book
--------------------------------------------------------------------------------
/source-code/ch05/library_app/tests/test_book.py:
--------------------------------------------------------------------------------
1 | from odoo.tests.common import TransactionCase
2 |
3 |
4 | class TestBook(TransactionCase):
5 | def setUp(self, *args, **kwargs):
6 | super().setUp(*args, **kwargs)
7 | user_admin = self.env.ref("base.user_admin")
8 | self.env = self.env(user=user_admin)
9 | self.Book = self.env['library.book']
10 | self.book1 = self.Book.create({
11 | "name": "Odoo Development Essentials",
12 | "isbn": "879-1-78439-279-6"
13 | })
14 |
15 | def test_book_create(self):
16 | "New Books are active by default"
17 | self.assertEqual(self.book1.active, True)
18 |
19 | def test_check_isbn(self):
20 | "Check valid ISBN"
21 | self.assertTrue(self.book1._check_isbn)
--------------------------------------------------------------------------------
/source-code/ch05/library_app/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Books
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Book Form
5 | library.book
6 |
7 |
27 |
28 |
29 |
30 |
31 | Book List
32 | library.book
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | Book Filters
45 | library.book
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Library Books
7 | library.book
8 | tree,form
9 |
10 |
11 |
15 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/views/templates.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
--------------------------------------------------------------------------------
/source-code/ch05/library_app/views/views.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
26 |
27 |
28 |
42 |
43 |
44 |
47 |
48 |
52 |
53 |
59 |
60 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/__init__.py:
--------------------------------------------------------------------------------
1 | from . import models
2 | from . import controllers
3 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/__manifest__.py:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Library Members",
3 | "license": "AGPL-3",
4 | "description": "Manage people who will be able to borrow books.",
5 | "author": "Alan Hou",
6 | "depends": ["library_app", "mail"],
7 | "application": False,
8 | "data": [
9 | "security/library_security.xml",
10 | "security/ir.model.access.csv",
11 | "views/book_view.xml",
12 | "views/member_view.xml",
13 | "views/library_menu.xml",
14 | "views/book_list_template.xml",
15 | ]
16 | }
--------------------------------------------------------------------------------
/source-code/ch05/library_member/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import main
2 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 | from odoo.addons.library_app.controllers.main import Books
3 |
4 |
5 | class BookExtended(Books):
6 | @http.route()
7 | def list(self, **kwargs):
8 | response = super().list(**kwargs)
9 | if kwargs.get("available"):
10 | all_books = response.qcontext["books"]
11 | available_books = all_books.filtered("is_available")
12 | response.qcontext["books"] = available_books
13 | return response
14 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/__init__.py:
--------------------------------------------------------------------------------
1 | from . import library_book
2 | from . import library_member
3 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/__pycache__/library_member.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch05/library_member/models/__pycache__/library_member.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Book(models.Model):
5 | _inherit = 'library.book'
6 | is_available = fields.Boolean('Is Available?')
7 | isbn = fields.Char(help="Use a valid ISBN-13 or ISBN-10.")
8 | publisher_id = fields.Many2one(index=True)
9 |
10 | def _check_isbn(self):
11 | self.ensure_one()
12 | isbn = self.isbn.replace('-', '')
13 | digits = [int(x) for x in isbn if x.isdigit()]
14 | if len(digits) == 10:
15 | ponderators = [1, 2, 3, 4, 5, 6, 7, 8, 9]
16 | total = sum(a * b for a, b in zip(digits[:9], ponderators))
17 | check = total % 11
18 | return digits[-1] == check
19 | else:
20 | return super()._check_isbn()
21 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/models/library_member.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Member(models.Model):
5 | _name = 'library.member'
6 | _description = 'Library Member'
7 | _inherit = ["mail.thread", "mail.activity.mixin"]
8 | # _inherits = {"res.partner": "partner_id"}
9 | card_number = fields.Char()
10 | partner_id = fields.Many2one(
11 | "res.partner",
12 | delegate=True,
13 | ondelete="cascade",
14 | required=True)
--------------------------------------------------------------------------------
/source-code/ch05/library_member/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_member_user,Member User Access,model_library_member,library_app.library_group_user,1,1,1,0
3 | access_member_manager,Member Manager Access,model_library_member,library_app.library_group_manager,1,1,1,1
--------------------------------------------------------------------------------
/source-code/ch05/library_member/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Librarian
5 |
6 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | (Not Available)
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Book: add Is Available? field
4 | library.book
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
--------------------------------------------------------------------------------
/source-code/ch05/library_member/views/member_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Library Member Form View
4 | library.member
5 |
6 |
19 |
20 |
21 |
22 | Library Member List View
23 | library.member
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import controllers
4 | from . import models
5 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | {
4 | "name": "Library Management",
5 | "summary": "Manage library catalog and book lending.",
6 | "author": "Alan Hou",
7 | "license": "AGPL-3",
8 | "category": "Services/Library",
9 | "website": "https://alanhou.org",
10 | "version": "15.0.1.0.0",
11 | "depends": ["base"],
12 | "data": [
13 | "security/library_security.xml",
14 | "security/ir.model.access.csv",
15 | "views/book_view.xml",
16 | "views/library_menu.xml",
17 | "views/book_list_template.xml",
18 | ],
19 | "demo": [
20 | "data/res.partner.csv",
21 | "data/library.book.csv",
22 | "data/book_demo.xml",
23 | ],
24 | "application": True,
25 | }
26 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import main
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/__pycache__/controllers.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/controllers/__pycache__/controllers.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/controllers.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # from odoo import http
3 |
4 |
5 | # class LibraryApp(http.Controller):
6 | # @http.route('/library_app/library_app', auth='public')
7 | # def index(self, **kw):
8 | # return "Hello, world"
9 |
10 | # @http.route('/library_app/library_app/objects', auth='public')
11 | # def list(self, **kw):
12 | # return http.request.render('library_app.listing', {
13 | # 'root': '/library_app/library_app',
14 | # 'objects': http.request.env['library_app.library_app'].search([]),
15 | # })
16 |
17 | # @http.route('/library_app/library_app/objects/', auth='public')
18 | # def object(self, obj, **kw):
19 | # return http.request.render('library_app.object', {
20 | # 'object': obj
21 | # })
22 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 |
3 |
4 | class Books(http.Controller):
5 | @http.route("/library/books")
6 | def list(self, **kwargs):
7 | Book = http.request.env['library.book']
8 | books = Book.search([])
9 | return http.request.render(
10 | "library_app.book_list_template",
11 | {"books": books}
12 | )
--------------------------------------------------------------------------------
/source-code/ch06/library_app/data/ library.book.csv:
--------------------------------------------------------------------------------
1 | "id","name","date_published","publisher_id/id","author_ids/id"
2 | library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
3 | library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"
--------------------------------------------------------------------------------
/source-code/ch06/library_app/data/book_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aldous Huxley
6 |
7 |
8 | Brave New World
9 |
11 | 1932-01-01
12 |
13 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/data/library.book.csv:
--------------------------------------------------------------------------------
1 | "id","name","date_published","publisher_id/id","author_ids/id"
2 | library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
3 | library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"
--------------------------------------------------------------------------------
/source-code/ch06/library_app/data/res.partner.csv:
--------------------------------------------------------------------------------
1 | id,name
2 | res_partner_alexandre,"Alexandre Fayolle"
3 | res_partner_daniel,"Daniel Reis"
4 | res_partner_holger,"Holger Brunn"
5 | res_partner_packt,"Packt Publishing"
--------------------------------------------------------------------------------
/source-code/ch06/library_app/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import library_book
4 | from . import res_partner
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/__pycache__/models.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/models/__pycache__/models.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/__pycache__/res_partner.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/models/__pycache__/res_partner.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models, api
2 | from odoo.exceptions import ValidationError
3 |
4 |
5 | class Book(models.Model):
6 | _name = 'library.book'
7 | _description = 'Book'
8 | _order = "name, date_published desc"
9 | _recname = "name"
10 | _table = "library_book"
11 | _log_access = True
12 | _auto = True
13 | # String fields
14 | name = fields.Char(
15 | "Title",
16 | default=None,
17 | help="Book cover title.",
18 | readonly=False,
19 | required=True,
20 | index=True,
21 | copy=False,
22 | deprecated=True,
23 | groups="",
24 | states={},
25 | )
26 | isbn = fields.Char('ISBN')
27 | book_type = fields.Selection(
28 | [("paper", "Paperback"),
29 | ("hard", "Hardcover"),
30 | ("electronic", "Electronic"),
31 | ("other", "Other")],
32 | "Type")
33 | notes = fields.Text("Internal Notes")
34 | desc = fields.Html("Description")
35 | # Numeric fields
36 | copies = fields.Integer(default=1)
37 | avg_rating = fields.Float("Average Rating", (3,2))
38 | price = fields.Monetary("Price", "currency_id")
39 | # price helper
40 | currency_id = fields.Many2one("res.currency")
41 | # Date and time fields
42 | date_published = fields.Date()
43 | last_borrow_date = fields.Datetime(
44 | "Last Borrowed On",
45 | default=lambda self: fields.Datetime.now())
46 | # Other fields:
47 | active = fields.Boolean('Active?', default=True)
48 | image = fields.Binary('Cover')
49 | # Relational fields
50 | publisher_id = fields.Many2one('res.partner', string='Publisher')
51 | author_ids = fields.Many2many('res.partner', string='Authors')
52 | publisher_country_id = fields.Many2one(
53 | "res.country", string="Publisher Country",
54 | related="publisher_id.country_id",
55 | readonly=False,
56 | # compute="_compute_publisher_country",
57 | # inverse="_inverse_publisher_country",
58 | # search="_search_publisher_country",
59 | )
60 |
61 | _sql_constraints = [
62 | ("library_book_name_uq",
63 | "UNIQUE (name, date_published)",
64 | "Title and publication date must be unique."),
65 | ("library_book_check_date",
66 | "CHECK (date_published <= current_date)",
67 | "Publication date must not be in the future."),
68 | ]
69 |
70 | @api.constrains("isbn")
71 | def _constrain_isbn_valid(self):
72 | for book in self:
73 | if book.isbn and not book._check_isbn():
74 | raise ValidationError("%s is an invalid ISBN" % book.isbn)
75 |
76 | def _inverse_publisher_country(self):
77 | for book in self:
78 | book.publisher_id.country_id = book.publisher_country_id
79 |
80 | def _search_publisher_country(self, operator, value):
81 | return [
82 | ("publisher_id.country_id", operator, value)
83 | ]
84 |
85 | @api.depends("publisher_id.country_id")
86 | def _compute_publisher_country(self):
87 | for book in self:
88 | book.publisher_country_id = book.publisher_id.country_id
89 |
90 | def _check_isbn(self):
91 | self.ensure_one()
92 | isbn = self.isbn.replace('-', '')
93 | digits = [int(x) for x in isbn if x.isdigit()]
94 | if len(digits) == 13:
95 | ponderations = [1, 3] * 6
96 | terms = [a * b for a, b in zip(digits[:12], ponderations)]
97 | remain = sum(terms) % 10
98 | check = 10 - remain if remain !=0 else 0
99 | return digits[-1] == check
100 |
101 | def button_check_isbn(self):
102 | for book in self:
103 | if not book.isbn:
104 | raise ValidationError("Please provide an ISBN for %s" % book.name)
105 | if book.isbn and not book._check_isbn():
106 | raise ValidationError("%s ISBN is invalid" % book.isbn)
107 | return True
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/library_book_category.py:
--------------------------------------------------------------------------------
1 | from odoo import api, fields, models
2 |
3 |
4 | class BookCategory(models.Model):
5 | _name = "library.book.category"
6 | _description = "Book Category"
7 | _parent_store = True
8 | name = fields.Char(translate=True, required=True)
9 | # Hierarchy fields
10 | parent_id = fields.Many2one(
11 | "library.book.category",
12 | "Parent Category",
13 | ondelete="restrict")
14 | parent_path = fields.Char(index=True)
15 | # Optional, but nice to have:
16 | child_ids = fields.Many2one(
17 | "library.book.category",
18 | "parent_id",
19 | "Subcategories")
20 | highlighted_id = fields.Reference(
21 | [('library.book', 'Book'),
22 | ('res.partner', 'Author')],
23 | 'Category Highlight'
24 | )
25 |
26 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/models/res_partner.py:
--------------------------------------------------------------------------------
1 | from odoo import models, fields
2 |
3 |
4 | class Partner(models.Model):
5 | _inherit = "res.partner"
6 | published_book_ids = fields.One2many(
7 | "library.book",
8 | "publisher_id",
9 | string="Published Books")
--------------------------------------------------------------------------------
/source-code/ch06/library_app/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_book_user,BookUser,model_library_book,library_group_user,1,1,1,0
3 | access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
4 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User
6 |
7 |
9 |
10 |
11 |
12 | Manager
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | Library Book User Access
23 |
24 |
25 | [('active', '=', True)]
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/static/description/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_app/static/description/icon.png
--------------------------------------------------------------------------------
/source-code/ch06/library_app/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_book
--------------------------------------------------------------------------------
/source-code/ch06/library_app/tests/test_book.py:
--------------------------------------------------------------------------------
1 | from odoo.tests.common import TransactionCase
2 |
3 |
4 | class TestBook(TransactionCase):
5 | def setUp(self, *args, **kwargs):
6 | super().setUp(*args, **kwargs)
7 | user_admin = self.env.ref("base.user_admin")
8 | self.env = self.env(user=user_admin)
9 | self.Book = self.env['library.book']
10 | self.book1 = self.Book.create({
11 | "name": "Odoo Development Essentials",
12 | "isbn": "879-1-78439-279-6"
13 | })
14 |
15 | def test_book_create(self):
16 | "New Books are active by default"
17 | self.assertEqual(self.book1.active, True)
18 |
19 | def test_check_isbn(self):
20 | "Check valid ISBN"
21 | self.assertTrue(self.book1._check_isbn)
--------------------------------------------------------------------------------
/source-code/ch06/library_app/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Books
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Book Form
5 | library.book
6 |
7 |
31 |
32 |
33 |
34 |
35 | Book List
36 | library.book
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Book Filters
49 | library.book
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Library Books
7 | library.book
8 | tree,form
9 |
10 |
11 |
15 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/views/templates.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
23 |
24 |
--------------------------------------------------------------------------------
/source-code/ch06/library_app/views/views.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
26 |
27 |
28 |
42 |
43 |
44 |
47 |
48 |
52 |
53 |
59 |
60 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/__init__.py:
--------------------------------------------------------------------------------
1 | from . import models
2 | from . import controllers
3 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/__manifest__.py:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Library Members",
3 | "license": "AGPL-3",
4 | "description": "Manage people who will be able to borrow books.",
5 | "author": "Alan Hou",
6 | "depends": ["library_app", "mail"],
7 | "application": False,
8 | "data": [
9 | "security/library_security.xml",
10 | "security/ir.model.access.csv",
11 | "views/book_view.xml",
12 | "views/member_view.xml",
13 | "views/library_menu.xml",
14 | "views/book_list_template.xml",
15 | ]
16 | }
--------------------------------------------------------------------------------
/source-code/ch06/library_member/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import main
2 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/controllers/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/controllers/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/controllers/__pycache__/main.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/controllers/__pycache__/main.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 | from odoo.addons.library_app.controllers.main import Books
3 |
4 |
5 | class BookExtended(Books):
6 | @http.route()
7 | def list(self, **kwargs):
8 | response = super().list(**kwargs)
9 | if kwargs.get("available"):
10 | all_books = response.qcontext["books"]
11 | available_books = all_books.filtered("is_available")
12 | response.qcontext["books"] = available_books
13 | return response
14 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/__init__.py:
--------------------------------------------------------------------------------
1 | from . import library_book
2 | from . import library_member
3 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/__pycache__/__init__.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/models/__pycache__/__init__.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/__pycache__/library_book.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/models/__pycache__/library_book.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/__pycache__/library_member.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch06/library_member/models/__pycache__/library_member.cpython-39.pyc
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Book(models.Model):
5 | _inherit = 'library.book'
6 | is_available = fields.Boolean('Is Available?')
7 | isbn = fields.Char(help="Use a valid ISBN-13 or ISBN-10.")
8 | publisher_id = fields.Many2one(index=True)
9 |
10 | def _check_isbn(self):
11 | self.ensure_one()
12 | isbn = self.isbn.replace('-', '')
13 | digits = [int(x) for x in isbn if x.isdigit()]
14 | if len(digits) == 10:
15 | ponderators = [1, 2, 3, 4, 5, 6, 7, 8, 9]
16 | total = sum(a * b for a, b in zip(digits[:9], ponderators))
17 | check = total % 11
18 | return digits[-1] == check
19 | else:
20 | return super()._check_isbn()
21 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/models/library_member.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Member(models.Model):
5 | _name = 'library.member'
6 | _description = 'Library Member'
7 | _inherit = ["mail.thread", "mail.activity.mixin"]
8 | # _inherits = {"res.partner": "partner_id"}
9 | card_number = fields.Char()
10 | partner_id = fields.Many2one(
11 | "res.partner",
12 | delegate=True,
13 | ondelete="cascade",
14 | required=True)
--------------------------------------------------------------------------------
/source-code/ch06/library_member/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_member_user,Member User Access,model_library_member,library_app.library_group_user,1,1,1,0
3 | access_member_manager,Member Manager Access,model_library_member,library_app.library_group_manager,1,1,1,1
--------------------------------------------------------------------------------
/source-code/ch06/library_member/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Librarian
5 |
6 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | (Not Available)
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Book: add Is Available? field
4 | library.book
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
--------------------------------------------------------------------------------
/source-code/ch06/library_member/views/member_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Library Member Form View
4 | library.member
5 |
6 |
19 |
20 |
21 |
22 | Library Member List View
23 | library.member
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/source-code/ch07/ch07_recorsets_code.py:
--------------------------------------------------------------------------------
1 |
2 | """
3 | >>> self
4 | res.users(1,)
5 | >>> self._name
6 | 'res.users'
7 | >>> self.name
8 | 'OdooBot'
9 | >>> self.login
10 | '__system__'
11 |
12 | >>> self.env
13 |
14 |
15 | >>> self.env["res.partner"].search([("display_name", "like", "Azure")])
16 | res.partner(14, 26, 33, 27)
17 |
18 | >>> self.env.context
19 | {'lang': 'en_US', 'tz': 'Europe/Brussels'}
20 |
21 | >>> self.env.ref('base.user_root')
22 | res.users(1,)
23 |
24 | >>> self.env['res.partner'].search([('display_name', 'like', 'Lumber')])
25 | res.partner(15, 34)
26 |
27 | >>> self.env['res.partner'].browse([15, 34])
28 | res.partner(15, 34)
29 | """
30 |
31 | >>> self.env["res.partner"].read_group([("display_name", "like", "Azure")], fields=["state_id:count_distinct",], groupby=["country_id"], lazy=False)
32 | [{'__count': 4, 'state_id': 1, 'country_id': (233, ), '__domain': ['&', ('country_id', '=', 233), ('display_name', 'like', 'Azure')]}]
33 | >>> self.env["res.country"].browse(233).name
34 | 'United States'
35 |
36 | >>> print(self.name)
37 | OdooBot
38 |
39 | >>> for rec in self: print(rec.name)
40 | ...
41 | OdooBot
42 |
43 | >>> self.company_id
44 | res.company(1,)
45 | >>> self.company_id.name
46 | 'YourCompany'
47 | >>> self.company_id.currency_id
48 | res.currency(1,)
49 | >>> self.company_id.currency_id.name
50 | 'EUR'
51 |
52 | >>> self.company_id.parent_id
53 | res.company()
54 | >>> self.company_id.parent_id.name
55 | False
56 |
57 | >>> self.browse(2).login_date
58 | datetime.datetime(2022, 5, 6, 3, 26, 21, 714562)
59 |
60 | >>> root = self.env["res.users"].browse(1)
61 | >>> print(root.name)
62 | OdooBot
63 | >>> root.name = "Superuser"
64 | >>> print(root.name)
65 | Superuser
66 |
67 | >>> from datetime import date
68 | >>> self.date = date(2020, 12, 1)
69 | >>> self.date
70 | datetime.date(2020, 12, 1)
71 | >>> self.date = "2020-12-02"
72 | >>> self.date
73 | datetime.date(2020, 12, 2)
74 |
75 | >>> import base64
76 | >>> blackdot_binary = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx\xdacd\xf8\x0f\x00\x01\x05\x01\x01'\x18\xe3f\x00\x00\x00\x00IEND\xaeB'\x82"
77 | >>> self.image_1920 = base64.b64encode(blackdot_binary).decode("utf-8")
78 |
79 | >>> self.child_ids = None
80 | >>> self.child_ids
81 | res.partner()
82 |
83 | >>> mycompany_partner = self.company_id.partner_id
84 | >>> myaddress = self.partner_id
85 | >>> mycompany_partner.child_ids = mycompany_partner.child_ids | myaddress
86 |
87 | >>> Partner = self.env["res.partner"]
88 | >>> recs = Partner.search([("name", "ilike", "Azure")])
89 | >>> recs.write({"comment": "Hello!"})
90 | True
91 |
92 | >>> self.write({ 'child_ids': address1 | address2})
93 |
94 | >>> Partner = self.env['res.partner']
95 | >>> new = Partner.create({'name': 'ACME', 'is_company': True})
96 | >>> print(new)
97 | res.partner(56,)
98 |
99 | >>> rec = Partner.search([('name', '=', 'ACME')])
100 | >>> rec.unlink()
101 | 2022-06-05 01:53:09,906 43 INFO odoo-dev odoo.models.unlink: User #1 deleted mail.message records with IDs: [22]
102 | 2022-06-05 01:53:09,952 43 INFO odoo-dev odoo.models.unlink: User #1 deleted res.partner records with IDs: [56]
103 | 2022-06-05 01:53:09,961 43 INFO odoo-dev odoo.models.unlink: User #1 deleted mail.followers records with IDs: [6]
104 | True
105 |
106 | >>> demo = self.env.ref("base.user_demo")
107 | >>> new = demo.copy({"name": "John", "login": "john@example.com"})
108 |
109 | >>> from datetime import date
110 | >>> date.today()
111 | datetime.date(2022, 6, 5)
112 | >>> from datetime import timedelta
113 | >>> date(2022, 6, 5) + timedelta(days=7)
114 | datetime.date(2022, 6, 12)
115 |
116 | >>> from dateutil.relativedelta import relativedelta
117 | >>> date(2022, 6, 5) + relativedelta(years=1, months=1)
118 | datetime.date(2023, 7, 5)
119 |
120 | >>> from odoo.tools import date_utils
121 | >>> from datetime import datetime
122 | >>> now = datetime(2022, 6, 5, 0, 0, 0)
123 | >>> date_utils.start_of(now, 'week')
124 | datetime.datetime(2022, 5, 30, 0, 0)
125 | >>> date_utils.end_of(now, 'week')
126 | datetime.datetime(2022, 6, 5, 23, 59, 59, 999999)
127 | >>> today = date(2022, 6, 5)
128 | >>> date_utils.add(today, months=2)
129 | datetime.date(2022, 8, 5)
130 | >>> date_utils.subtract(today, months=2)
131 | datetime.date(2022, 4, 5)
132 |
133 | >>> date(2022, 6, 5).strftime("%d/%m/%Y")
134 | '05/06/2022'
135 |
136 | >>> from odoo import fields
137 | >>> fields.Datetime.to_datetime("2020-11-21 23:11:55")
138 | datetime.datetime(2020, 11, 21, 23, 11, 55)
139 |
140 | >>> from datetime import datetime
141 | >>> datetime.strptime("03/11/2020", "%d/%m/%Y")
142 | datetime.datetime(2020, 11, 3, 0, 0)
143 |
144 | >>> from datetime import datetime
145 | >>> import pytz
146 | >>> naive_date = datetime(2020, 12, 1, 0, 30, 0)
147 | >>> client_tz = self.env.context["tz"]
148 | >>> client_date = pytz.timezone(client_tz).localize(naive_date)
149 | >>> utc_date = client_date.astimezone(pytz.utc)
150 | >>> print(utc_date)
151 | 2020-11-30 23:30:00+00:00
152 |
153 |
154 | >>> rs0 = self.env["res.partner"].search([("display_name", "like", "Azure")])
155 | >>> len(rs0)
156 | 4
157 | >>> rs0.filtered(lambda r: r.name.startswith("Nicole"))
158 | res.partner(27,)
159 | >>> rs0.filtered("is_company")
160 | res.partner(14,)
161 | >>> rs0.mapped("name")
162 | ['Azure Interior', 'Brandon Freeman', 'Colleen Diaz', 'Nicole Ford']
163 | >>> rs0.sorted("name", reverse=True).mapped("name")
164 | ['Nicole Ford', 'Colleen Diaz', 'Brandon Freeman', 'Azure Interior']
165 | >>> rs0.mapped(lambda r: (r.id, r.name))
166 | [(14, 'Azure Interior'), (26, 'Brandon Freeman'), (33, 'Colleen Diaz'), (27, 'Nicole Ford')]
167 |
168 | >>> Partner = self.env['res.partner']
169 | >>> recs = self.env['res.partner']>>> for i in range(3):... rec = Partner.create({"name": "Partner %s" % i})
170 | ... recs |= rec
171 | ...
172 | >>> print(recs)res.partner(58, 59, 60)
173 |
174 |
175 | >>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%s OR id=%s", ("demo", 1))
176 | >>> self.env.cr.execute("SELECT id, login FROM res_users WHERE login=%(login)s OR id=%(id)s", {"login": "demo", "id": 1})
177 |
178 | >>> self.env.cr.fetchall()
179 | [(1, '__system__'), (6, 'demo')]
180 |
181 | >>> self.env.cr.dictfetchall()
182 | [{'id': 1, 'login': '__system__'}, {'id': 6, 'login': 'demo'}]
183 |
184 |
185 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import controllers
4 | from . import models
5 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/__manifest__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | {
4 | "name": "Library Management",
5 | "summary": "Manage library catalog and book lending.",
6 | "author": "Alan Hou",
7 | "license": "AGPL-3",
8 | "category": "Services/Library",
9 | "website": "https://alanhou.org",
10 | "version": "15.0.1.0.0",
11 | "depends": ["base"],
12 | "data": [
13 | "security/library_security.xml",
14 | "security/ir.model.access.csv",
15 | "views/book_view.xml",
16 | "views/library_menu.xml",
17 | "views/book_list_template.xml",
18 | ],
19 | "demo": [
20 | "data/res.partner.csv",
21 | "data/library.book.csv",
22 | "data/book_demo.xml",
23 | ],
24 | "application": True,
25 | }
26 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import main
--------------------------------------------------------------------------------
/source-code/ch08/library_app/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 |
3 |
4 | class Books(http.Controller):
5 | @http.route("/library/books")
6 | def list(self, **kwargs):
7 | Book = http.request.env['library.book']
8 | books = Book.search([])
9 | return http.request.render(
10 | "library_app.book_list_template",
11 | {"books": books}
12 | )
--------------------------------------------------------------------------------
/source-code/ch08/library_app/data/book_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Aldous Huxley
6 |
7 |
8 | Brave New World
9 |
11 | 1932-01-01
12 |
13 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/data/library.book.csv:
--------------------------------------------------------------------------------
1 | "id","name","date_published","publisher_id/id","author_ids/id"
2 | library_book_ode11,"Odoo Development Essentials 11","2018-03-01",res_partner_packt,res_partner_daniel
3 | library_book_odc11,"Odoo 11 Development Cookbook","2018-01-01",res_partner_packt,"res_partner_alexandre,res_partner_holger"
--------------------------------------------------------------------------------
/source-code/ch08/library_app/data/res.partner.csv:
--------------------------------------------------------------------------------
1 | id,name
2 | res_partner_alexandre,"Alexandre Fayolle"
3 | res_partner_daniel,"Daniel Reis"
4 | res_partner_holger,"Holger Brunn"
5 | res_partner_packt,"Packt Publishing"
--------------------------------------------------------------------------------
/source-code/ch08/library_app/demo/demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/models/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from . import library_book
4 | from . import res_partner
--------------------------------------------------------------------------------
/source-code/ch08/library_app/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models, api
2 | from odoo.exceptions import ValidationError
3 |
4 |
5 | class Book(models.Model):
6 | _name = 'library.book'
7 | _description = 'Book'
8 | _order = "name, date_published desc"
9 | _recname = "name"
10 | _table = "library_book"
11 | _log_access = True
12 | _auto = True
13 | # String fields
14 | name = fields.Char(
15 | "Title",
16 | default=None,
17 | help="Book cover title.",
18 | readonly=False,
19 | required=True,
20 | index=True,
21 | copy=False,
22 | deprecated=True,
23 | groups="",
24 | states={},
25 | )
26 | isbn = fields.Char('ISBN')
27 | book_type = fields.Selection(
28 | [("paper", "Paperback"),
29 | ("hard", "Hardcover"),
30 | ("electronic", "Electronic"),
31 | ("other", "Other")],
32 | "Type")
33 | notes = fields.Text("Internal Notes")
34 | desc = fields.Html("Description")
35 | # Numeric fields
36 | copies = fields.Integer(default=1)
37 | avg_rating = fields.Float("Average Rating", (3,2))
38 | price = fields.Monetary("Price", "currency_id")
39 | # price helper
40 | currency_id = fields.Many2one("res.currency")
41 | # Date and time fields
42 | date_published = fields.Date()
43 | last_borrow_date = fields.Datetime(
44 | "Last Borrowed On",
45 | default=lambda self: fields.Datetime.now())
46 | # Other fields:
47 | active = fields.Boolean('Active?', default=True)
48 | image = fields.Binary('Cover')
49 | # Relational fields
50 | publisher_id = fields.Many2one('res.partner', string='Publisher')
51 | author_ids = fields.Many2many('res.partner', string='Authors')
52 | publisher_country_id = fields.Many2one(
53 | "res.country", string="Publisher Country",
54 | related="publisher_id.country_id",
55 | readonly=False,
56 | # compute="_compute_publisher_country",
57 | # inverse="_inverse_publisher_country",
58 | # search="_search_publisher_country",
59 | )
60 |
61 | _sql_constraints = [
62 | ("library_book_name_uq",
63 | "UNIQUE (name, date_published)",
64 | "Title and publication date must be unique."),
65 | ("library_book_check_date",
66 | "CHECK (date_published <= current_date)",
67 | "Publication date must not be in the future."),
68 | ]
69 |
70 | @api.constrains("isbn")
71 | def _constrain_isbn_valid(self):
72 | for book in self:
73 | if book.isbn and not book._check_isbn():
74 | raise ValidationError("%s is an invalid ISBN" % book.isbn)
75 |
76 | def _inverse_publisher_country(self):
77 | for book in self:
78 | book.publisher_id.country_id = book.publisher_country_id
79 |
80 | def _search_publisher_country(self, operator, value):
81 | return [
82 | ("publisher_id.country_id", operator, value)
83 | ]
84 |
85 | @api.depends("publisher_id.country_id")
86 | def _compute_publisher_country(self):
87 | for book in self:
88 | book.publisher_country_id = book.publisher_id.country_id
89 |
90 | def _check_isbn(self):
91 | self.ensure_one()
92 | isbn = self.isbn.replace('-', '')
93 | digits = [int(x) for x in isbn if x.isdigit()]
94 | if len(digits) == 13:
95 | ponderations = [1, 3] * 6
96 | terms = [a * b for a, b in zip(digits[:12], ponderations)]
97 | remain = sum(terms) % 10
98 | check = 10 - remain if remain !=0 else 0
99 | return digits[-1] == check
100 |
101 | def button_check_isbn(self):
102 | for book in self:
103 | if not book.isbn:
104 | raise ValidationError("Please provide an ISBN for %s" % book.name)
105 | if book.isbn and not book._check_isbn():
106 | raise ValidationError("%s ISBN is invalid" % book.isbn)
107 | return True
--------------------------------------------------------------------------------
/source-code/ch08/library_app/models/library_book_category.py:
--------------------------------------------------------------------------------
1 | from odoo import api, fields, models
2 |
3 |
4 | class BookCategory(models.Model):
5 | _name = "library.book.category"
6 | _description = "Book Category"
7 | _parent_store = True
8 | name = fields.Char(translate=True, required=True)
9 | # Hierarchy fields
10 | parent_id = fields.Many2one(
11 | "library.book.category",
12 | "Parent Category",
13 | ondelete="restrict")
14 | parent_path = fields.Char(index=True)
15 | # Optional, but nice to have:
16 | child_ids = fields.Many2one(
17 | "library.book.category",
18 | "parent_id",
19 | "Subcategories")
20 | highlighted_id = fields.Reference(
21 | [('library.book', 'Book'),
22 | ('res.partner', 'Author')],
23 | 'Category Highlight'
24 | )
25 |
26 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/models/res_partner.py:
--------------------------------------------------------------------------------
1 | from odoo import models, fields
2 |
3 |
4 | class Partner(models.Model):
5 | _inherit = "res.partner"
6 | published_book_ids = fields.One2many(
7 | "library.book",
8 | "publisher_id",
9 | string="Published Books")
--------------------------------------------------------------------------------
/source-code/ch08/library_app/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_book_user,BookUser,model_library_book,library_group_user,1,1,1,0
3 | access_book_manager,BookManager,model_library_book,library_group_manager,1,1,1,1
4 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User
6 |
7 |
9 |
10 |
11 |
12 | Manager
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 | Library Book User Access
23 |
24 |
25 | [('active', '=', True)]
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/static/description/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iTranslateX/odoo-essentials/d740941b89fc016b46b1ced11500be9b16f67270/source-code/ch08/library_app/static/description/icon.png
--------------------------------------------------------------------------------
/source-code/ch08/library_app/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_book
--------------------------------------------------------------------------------
/source-code/ch08/library_app/tests/test_book.py:
--------------------------------------------------------------------------------
1 | from odoo.tests.common import TransactionCase
2 |
3 |
4 | class TestBook(TransactionCase):
5 | def setUp(self, *args, **kwargs):
6 | super().setUp(*args, **kwargs)
7 | user_admin = self.env.ref("base.user_admin")
8 | self.env = self.env(user=user_admin)
9 | self.Book = self.env['library.book']
10 | self.book1 = self.Book.create({
11 | "name": "Odoo Development Essentials",
12 | "isbn": "879-1-78439-279-6"
13 | })
14 |
15 | def test_book_create(self):
16 | "New Books are active by default"
17 | self.assertEqual(self.book1.active, True)
18 |
19 | def test_check_isbn(self):
20 | "Check valid ISBN"
21 | self.assertTrue(self.book1._check_isbn)
--------------------------------------------------------------------------------
/source-code/ch08/library_app/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Books
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Book Form
5 | library.book
6 |
7 |
31 |
32 |
33 |
34 |
35 | Book List
36 | library.book
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | Book Filters
49 | library.book
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/source-code/ch08/library_app/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Library Books
7 | library.book
8 | tree,form
9 |
10 |
11 |
15 |
19 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/__init__.py:
--------------------------------------------------------------------------------
1 | from . import models
2 | from . import wizard
3 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/__manifest__.py:
--------------------------------------------------------------------------------
1 | {
2 | 'name': 'Library Book Borrowing',
3 | 'description': 'Members can borrow books from the library.',
4 | 'author': 'Alan Hou',
5 | 'depends': ['library_member', 'mail'],
6 | 'data':[
7 | 'security/ir.model.access.csv',
8 | "wizard/checkout_mass_message_wizard_view.xml",
9 | 'views/library_menu.xml',
10 | 'views/checkout_view.xml',
11 | 'data/library_checkout_stage.xml',
12 | ],
13 | }
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/data/library_checkout_stage.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Draft
4 | 10
5 | new
6 |
7 |
8 | Borrowed
9 | 20
10 | open
11 |
12 |
13 | Completed
14 | 90
15 | done
16 |
17 |
18 | Canceled
19 | 95
20 | cancel
21 |
22 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/models/__init__.py:
--------------------------------------------------------------------------------
1 | from . import library_checkout_stage
2 | from . import library_checkout
3 | from . import library_checkout_line
4 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/models/library_checkout.py:
--------------------------------------------------------------------------------
1 | from odoo import api, fields, models, exceptions
2 |
3 |
4 | class Checkout(models.Model):
5 | _name = "library.checkout"
6 | _description = "Checkout Request"
7 | _inherit = ["mail.thread", "mail.activity.mixin"]
8 | member_id = fields.Many2one(
9 | "library.member",
10 | required=True,
11 | )
12 | user_id = fields.Many2one(
13 | "res.users",
14 | "Librarian",
15 | default=lambda s: s.env.user,
16 | )
17 | request_date = fields.Date(
18 | default=lambda s: fields.Date.today(),
19 | compute="_compute_request_date_onchange",
20 | store=True,
21 | readonly=False,
22 | )
23 | line_ids = fields.One2many(
24 | 'library.checkout.line',
25 | 'checkout_id',
26 | string="Borrowed Books",
27 | )
28 |
29 | @api.model
30 | def _default_stage_id(self):
31 | Stage = self.env["library.checkout.stage"]
32 | return Stage.search([("state", "=", "new")], limit=1)
33 | stage_id = fields.Many2one(
34 | "library.checkout.stage",
35 | default=_default_stage_id,
36 | group_expand="_group_expand_stage_id")
37 | state = fields.Selection(related="stage_id.state")
38 | checkout_date = fields.Date(readonly=True)
39 | close_date = fields.Date(readonly=True)
40 |
41 | @api.depends("member_id")
42 | def _compute_request_date_onchange(self):
43 | today_date = fields.Date.today()
44 | if self.request_date != today_date:
45 | self.request_date = today_date
46 | return {
47 | "warning": {
48 | "title": "Changed Request Date",
49 | "message": "Request date changed to today!",
50 | }
51 | }
52 |
53 | # @api.onchange("member_id")
54 | # def onchange_member_id(self):
55 | # today_date = fields.Date.today()
56 | # if self.request_date != today_date:
57 | # self.request_date = today_date
58 | # return {
59 | # "warning": {
60 | # "title": "Changed Request Date",
61 | # "message": "Request date changed to today!",
62 | # }
63 | # }
64 |
65 | @api.model
66 | def _group_expand_stage_id(self, stages, domain, order):
67 | return stages.search([], order=order)
68 |
69 | @api.model
70 | def create(self, vals):
71 | # 创建前执行的代码,应使用vals字典
72 | new_record = super().create(vals)
73 | # 创建后执行的代码,应使用new_record
74 | if new_record.stage_id.state in ('open', 'close'):
75 | raise exceptions.UserError("State not allowed for new checkouts.")
76 | return new_record
77 |
78 | def write(self, vals):
79 | # 写入之前的代码,self为老值
80 | if "stage_id" in vals:
81 | Stage = self.env["library.checkout.stage"]
82 | old_state = self.stage_id.state
83 | new_state = Stage.browse(vals["stage_id"]).state
84 | if new_state != old_state and new_state == "open":
85 | vals["checkout_date"] = fields.Date.today()
86 | if new_state != old_state and new_state == "done":
87 | vals["close_date"] = fields.Date.today()
88 | # old_state = self.stage_id.state
89 | super().write(vals)
90 | # 写入之后的代码,可使用更新后的self
91 | # new_state = self.stage_id.state
92 | # if not self.env.context.get("_checkout_write"):
93 | # if new_state != old_state and new_state == "open":
94 | # self.with_context(_checkout_write=True).write(
95 | # {"checkout_date": fields.Date.today()})
96 | # if new_state != old_state and new_state == "done":
97 | # self.with_context(_checkout_write=True).write(
98 | # {"close_date": fields.Date.today()})
99 | return True
100 |
101 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/models/library_checkout_line.py:
--------------------------------------------------------------------------------
1 | from odoo import api, exceptions, fields, models
2 |
3 |
4 | class CheckoutLine(models.Model):
5 | _name = "library.checkout.line"
6 | _description = "Checkout Request Line"
7 | checkout_id = fields.Many2one(
8 | "library.checkout",
9 | required=True,
10 | )
11 | book_id = fields.Many2one("library.book", required=True)
12 | note = fields.Char("Notes")
13 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/models/library_checkout_stage.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class CheckoutStage(models.Model):
5 | _name = "library.checkout.stage"
6 | _description = "Checkout Stage"
7 | _order = "sequence"
8 | name = fields.Char()
9 | sequence = fields.Integer(default=10)
10 | fold = fields.Boolean()
11 | active = fields.Boolean(default=True)
12 | state = fields.Selection(
13 | [("new", "Requested"),
14 | ("open", "Borrowed"),
15 | ("done", "Returned"),
16 | ("cancel", "Canceled")],
17 | default="new",
18 | )
19 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | checkout_user,Checkout User,model_library_checkout,library_app.library_group_user,1,1,1,1
3 | checkout_line_user,Checkout Line User,model_library_checkout_line,library_app.library_group_user,1,1,1,1
4 | checkout_stage_user,Checkout Stage User,model_library_checkout_stage,library_app.library_group_user,1,0,0,0
5 | checkout_stage_manager,Checkout Stage Manager,model_library_checkout_stage,library_app.library_group_manager,1,1,1,1
6 | checkout_massmessage_user,Checkout Mass Message User,model_library_checkout_massmessage,library_app.library_group_user,1,1,1,1
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from . import test_checkout_mass_message
2 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/tests/test_checkout_mass_message.py:
--------------------------------------------------------------------------------
1 | from odoo import exceptions
2 | from odoo.tests import common
3 |
4 |
5 | class TestWizard(common.SingleTransactionCase):
6 | def setUp(self,*args, **kwargs):
7 | super(TestWizard, self).setUp(*args, **kwargs)
8 | # 配置测试数据
9 | admin_user = self.env.ref("base.user_admin")
10 | self.Checkout = self.env["library.checkout"].with_user(admin_user)
11 | self.Wizard = self.env["library.checkout.massmessage"].with_user(admin_user)
12 | a_member = self.env["library.member"].create({"partner_id": admin_user.partner_id.id})
13 | self.checkout0 = self.Checkout.create({"member_id": a_member.id})
14 |
15 | def test_01_button_send(self):
16 | """发送按钮应对借阅记录创建消息"""
17 | count_before = len(self.checkout0.message_ids)
18 | Wizard0 = self.Wizard.with_context(active_ids=self.checkout0.ids)
19 | wizard0 = Wizard0.create({
20 | "message_subject": "Hello",
21 | "message_body": "This is a message.",
22 | })
23 | wizard0.button_send()
24 | count_after = len(self.checkout0.message_ids)
25 | self.assertEqual(
26 | count_before + 1,
27 | count_after,
28 | "Expected one additional message in the Checkout.",
29 | )
30 |
31 | def test_02_button_send_empty_body(self):
32 | """消息体而空时发送按钮报错"""
33 | Wizard0 = self.Wizard.with_context(active_ids=self.checkout0.ids)
34 | wizard0 = Wizard0.create({})
35 | with self.assertRaises(exceptions.UserError) as e:
36 | wizard0.button_send()
37 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/views/checkout_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Checkout Tree
4 | library.checkout
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Checkout Form
14 | library.checkout
15 |
16 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Checkouts
4 | library.checkout
5 | tree,form
6 |
7 |
11 |
12 | Stages
13 | library.checkout.stage
14 | tree,form
15 |
16 |
20 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/wizard/__init__.py:
--------------------------------------------------------------------------------
1 | from . import checkout_mass_message
2 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/wizard/checkout_mass_message.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from odoo import api, exceptions, fields, models
3 |
4 |
5 | _logger = logging.getLogger(__name__)
6 |
7 |
8 | class CheckoutMassMessage(models.TransientModel):
9 | _name = "library.checkout.massmessage"
10 | _description = "Send Message to Borrowers"
11 | checkout_ids = fields.Many2many(
12 | "library.checkout",
13 | string="Checkouts"
14 | )
15 | message_subject = fields.Char()
16 | message_body = fields.Html()
17 |
18 | @api.model
19 | def default_get(self, field_names):
20 | defaults_dict = super().default_get(field_names)
21 | checkout_ids = self.env.context["active_ids"]
22 | defaults_dict["checkout_ids"] = [(6, 0, checkout_ids)]
23 | return defaults_dict
24 |
25 | def button_send(self):
26 | # import pdb; pdb.set_trace()
27 | self.ensure_one()
28 | if not self.checkout_ids:
29 | raise exceptions.UserError("No checkouts were selected")
30 | if not self.message_body:
31 | raise exceptions.UserError("A message body is required")
32 | for checkout in self.checkout_ids:
33 | checkout.message_post(
34 | body=self.message_body,
35 | subject=self.message_subject,
36 | subtype_xmlid="mail.mt_comment"
37 | )
38 | _logger.debug(
39 | "Message on %d to followers: %s",
40 | checkout.id,
41 | checkout.message_follower_ids)
42 | _logger.info(
43 | "Posted %d messages to the Checkouts: %s",
44 | len(self.checkout_ids),
45 | str(self.checkout_ids),
46 | )
47 | return True
48 |
--------------------------------------------------------------------------------
/source-code/ch08/library_checkout/wizard/checkout_mass_message_wizard_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Library Checkout Mass Message Wizard
4 | library.checkout.massmessage
5 |
6 |
17 |
18 |
19 |
20 | Send Messages
21 | library.checkout.massmessage
22 | form
23 |
24 | form,list
25 | new
26 |
27 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/__init__.py:
--------------------------------------------------------------------------------
1 | from . import models
2 | from . import controllers
3 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/__manifest__.py:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Library Members",
3 | "license": "AGPL-3",
4 | "description": "Manage people who will be able to borrow books.",
5 | "author": "Alan Hou",
6 | "depends": ["library_app", "mail"],
7 | "application": False,
8 | "data": [
9 | "security/library_security.xml",
10 | "security/ir.model.access.csv",
11 | "views/book_view.xml",
12 | "views/member_view.xml",
13 | "views/library_menu.xml",
14 | "views/book_list_template.xml",
15 | ]
16 | }
--------------------------------------------------------------------------------
/source-code/ch08/library_member/controllers/__init__.py:
--------------------------------------------------------------------------------
1 | from . import main
2 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/controllers/main.py:
--------------------------------------------------------------------------------
1 | from odoo import http
2 | from odoo.addons.library_app.controllers.main import Books
3 |
4 |
5 | class BookExtended(Books):
6 | @http.route()
7 | def list(self, **kwargs):
8 | response = super().list(**kwargs)
9 | if kwargs.get("available"):
10 | all_books = response.qcontext["books"]
11 | available_books = all_books.filtered("is_available")
12 | response.qcontext["books"] = available_books
13 | return response
14 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/models/__init__.py:
--------------------------------------------------------------------------------
1 | from . import library_book
2 | from . import library_member
3 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/models/library_book.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Book(models.Model):
5 | _inherit = 'library.book'
6 | is_available = fields.Boolean('Is Available?')
7 | isbn = fields.Char(help="Use a valid ISBN-13 or ISBN-10.")
8 | publisher_id = fields.Many2one(index=True)
9 |
10 | def _check_isbn(self):
11 | self.ensure_one()
12 | isbn = self.isbn.replace('-', '')
13 | digits = [int(x) for x in isbn if x.isdigit()]
14 | if len(digits) == 10:
15 | ponderators = [1, 2, 3, 4, 5, 6, 7, 8, 9]
16 | total = sum(a * b for a, b in zip(digits[:9], ponderators))
17 | check = total % 11
18 | return digits[-1] == check
19 | else:
20 | return super()._check_isbn()
21 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/models/library_member.py:
--------------------------------------------------------------------------------
1 | from odoo import fields, models
2 |
3 |
4 | class Member(models.Model):
5 | _name = 'library.member'
6 | _description = 'Library Member'
7 | _inherit = ["mail.thread", "mail.activity.mixin"]
8 | # _inherits = {"res.partner": "partner_id"}
9 | card_number = fields.Char()
10 | partner_id = fields.Many2one(
11 | "res.partner",
12 | delegate=True,
13 | ondelete="cascade",
14 | required=True)
--------------------------------------------------------------------------------
/source-code/ch08/library_member/security/ir.model.access.csv:
--------------------------------------------------------------------------------
1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
2 | access_member_user,Member User Access,model_library_member,library_app.library_group_user,1,1,1,0
3 | access_member_manager,Member Manager Access,model_library_member,library_app.library_group_manager,1,1,1,1
--------------------------------------------------------------------------------
/source-code/ch08/library_member/security/library_security.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Librarian
5 |
6 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/views/book_list_template.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | (Not Available)
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/views/book_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Book: add Is Available? field
4 | library.book
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/views/library_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
--------------------------------------------------------------------------------
/source-code/ch08/library_member/views/member_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Library Member Form View
4 | library.member
5 |
6 |
19 |
20 |
21 |
22 | Library Member List View
23 | library.member
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/source-code/ch09/client_app/library.py:
--------------------------------------------------------------------------------
1 | from argparse import ArgumentParser
2 | # from library_xmlrpc import LibraryAPI
3 | from library_odoorpc import LibraryAPI
4 |
5 | parser = ArgumentParser()
6 | parser.add_argument(
7 | "command",
8 | choices=["list", "add", "set", "del"])
9 | parser.add_argument("params", nargs="*") # 可选参数
10 | args = parser.parse_args()
11 |
12 | host, port, db = "localhost", 8069, "odoo-dev"
13 | user, pwd = "admin", "admin"
14 | api = LibraryAPI(host, port, db, user, pwd)
15 |
16 |
17 | if args.command == "list":
18 | title = args.params[:1]
19 | if len(title) != 0:
20 | title = title[0]
21 | books = api.search_read(title)
22 | for book in books:
23 | print("%(id)d %(name)s" % book)
24 |
25 | if args.command == "add":
26 | title = args.params[0]
27 | book_id = api.create(title)
28 | print("Book added with ID %d for title %s." % (book_id, title))
29 |
30 | if args.command == "set":
31 | if len(args.params) != 2:
32 | print("set command requires a Title and ID.")
33 | else:
34 | book_id, title = int(args.params[0]), args.params[1]
35 | api.write(book_id, title)
36 | print("Title of Book ID %d set to %s." % (book_id, title))
37 |
38 | if args.command == "del":
39 | book_id = int(args.params[0])
40 | api.unlink(book_id)
41 | print("Book with ID %s was deleted." % book_id)
--------------------------------------------------------------------------------
/source-code/ch09/client_app/library_odoorpc.py:
--------------------------------------------------------------------------------
1 | import odoorpc
2 |
3 |
4 | class LibraryAPI():
5 | def __init__(self, host, port, db, user, pwd):
6 | self.api = odoorpc.ODOO(host, port=port)
7 | self.api.login(db, user, pwd)
8 | self.uid = self.api.env.uid
9 | self.model = "library.book"
10 | self.Model = self.api.env[self.model]
11 |
12 | def _execute(self, method, arg_list, kwarg_dict=None):
13 | return self.api.execute(
14 | self.model,
15 | method, *arg_list, **kwarg_dict)
16 |
17 | def search_read(self, title=None):
18 | domain = [("name", "ilike", title)] if title else []
19 | fields = ["id", "name"]
20 | return self.Model.search_read(domain, fields)
21 |
22 | def create(self, title):
23 | vals = {"name": title}
24 | return self.Model.create(vals)
25 |
26 | def write(self, id, title):
27 | vals = {"name": title}
28 | self.Model.write(id, vals)
29 |
30 | def unlink(self, id):
31 | return self.Model.unlink(id)
32 |
33 |
34 | if __name__ == "__main__":
35 | # 测试配置
36 | host, port, db = "localhost", 8069, "odoo-dev"
37 | user, pwd = "admin", "admin"
38 | api = LibraryAPI(host, port, db, user, pwd)
39 | from pprint import pprint
40 | pprint(api.search_read())
--------------------------------------------------------------------------------
/source-code/ch09/client_app/library_xmlrpc.py:
--------------------------------------------------------------------------------
1 | import xmlrpc.client
2 |
3 |
4 | class LibraryAPI:
5 | def __init__(self, host, port, db, user, pwd):
6 | common = xmlrpc.client.ServerProxy(
7 | "http://%s:%d/xmlrpc/2/common" % (host, port))
8 | self.api = xmlrpc.client.ServerProxy(
9 | "http://%s:%d/xmlrpc/2/object" % (host, port))
10 | self.uid = common.authenticate(db, user, pwd, {})
11 | self.pwd = pwd
12 | self.db = db
13 | self.model = "library.book"
14 |
15 | def _execute(self, method, arg_list, kwarg_dict=None):
16 | return self.api.execute_kw(
17 | self.db, self.uid, self.pwd, self.model,
18 | method, arg_list, kwarg_dict or {})
19 |
20 | def search_read(self, title=None):
21 | domain = [("name", "ilike", title)] if title else []
22 | fields = ["id", "name"]
23 | return self._execute("search_read", [domain, fields])
24 |
25 | def create(self, title):
26 | vals = {"name": title}
27 | return self._execute("create", [vals])
28 |
29 | def write(self, id, title):
30 | vals = {"name": title}
31 | return self._execute("write", [[id], vals])
32 |
33 | def unlink(self, id):
34 | return self._execute("unlink", [[id]])
35 |
36 |
37 | if __name__ == "__main__":
38 | # 测试配置
39 | host, port, db = "localhost", 8069, "odoo-dev"
40 | user, pwd = "admin", "admin"
41 | api = LibraryAPI(host, port, db, user, pwd)
42 | from pprint import pprint
43 |
44 | pprint(api.search_read())
45 |
--------------------------------------------------------------------------------