本文为最好用的免费ERP系统Odoo 12开发手册系列文章第十三篇。
Odoo 起初是一个后台系统,但很快就有了前端界面的需求。早期基于后台界面的门户界面不够灵活并且对移动端不友好。为解决这一问题,Odoo 引入了新的网站功能,为系统添加了 CMS(Content Management System)内容管理系统。这使得我们无需集成第三方 CMS 便可创建美观又高效的前端。本文中我们将学习如何利用 Odoo 自带的网站功能开发面向前端的插件模块。
本文主要内容有:
我将用第十一章 Odoo 12开发之看板视图和用户端 QWeb中最后编辑的library_checkout插件模块,代码请见GitHub 仓库。本文完成后的代码也请参见GitHub 仓库。
本文中我们将为图书会员添加一个自助服务功能。可供会员分别登录账号来访问他们的借阅请求列表。这样我们就可以学习网站开发的基本技术:创建动态页面、在页面间传递参数、创建表单以及处理表单数据验证。对这些新的图书网站功能,我们要新建一个插件模块library_website。
大家应该已经轻车熟路了,首先创建插件的声明文件ibrary_website/__manifest__.py,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ 'name': 'Library Website', 'description': 'Create and check book checkout requests.', 'author': 'Alan Hou', 'depends': [ 'library_checkout' ], 'data': [ 'security/ir.model.access.csv', 'security/library_security.xml', 'views/library_member.xml', ], } |
网站功能将会依赖于library_checkout。我们并没有添加对website核心插件模块的依赖。website插件为创建完整功能的网站提供了有用的框架,但现在我们仅探讨核心框架自带的基础网站功能,尚无需使用website。我们想要图书会员通过登录信息在图书网站上访问自己的借阅请求。为此需要在图书会员模型中添加一个user_id字段,需要分别在模型和视图中添加,下面就开始进行网站的创建:
1、添加library_website/models/library_member.py文件
1 2 3 4 5 |
from odoo import fields, models class Member(models.Model): _inherit = 'library.member' user_id = fields.Many2one('res.users') |
2、添加library_website/models/__init__.py文件:
1 |
from . import library_member |
3、添加library_website/__init__.py文件:
1 |
from . import models |
4、添加library_website/views/library_member.xml文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
访问这些网页的都是门户用户,无需访问后台菜单。我们需要为这个用户组设置安全访问权限,否则会在使用图书网站功能时报权限错误。
5、添加library_website/security/ir.model.access.csv文件,添加对图书模型的读权限:
1 2 3 4 5 6 7 |
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_book_portal,Book Portal Access,library_app.model_library_book,base.group_ portal,1,0,0,0 access_member_portal,Member Portal Access,library_member.model_library_member,ba se.group_portal,1,0,0,0 access_checkout_portal,Checkout Portal Access,library_checkout.model_library_che ckout,base.group_portal,1,0,0,0 |
6、在library_website/security/library_security.xml文件中添加记录规则来限制门户用户所能访问的记录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[('user_id', '=', user.id)]
[('member_id.user_id', '=', user.id)]
|
base.group_portal是门户用户组的标识符。在创建门户用户时,应设置他们的用户类型为 Portal,而不是Internal User。这会让他们属于门户用户组并继承我们上面定义的访问权限:
补充:以上内容需开启开发者模式才可见
一旦为图书会员创建了一个门户用户,就应在我们会员表单中的用户字段中使用。该登录信息将可以访问相应会员的借阅请求。
小贴士:在模型中使用 ACL 和记录规则来实现安全权限比使用控制器的逻辑要更为安全。这是因为攻击者有可能跳过网页控制器直接使用RPC 来访问模型 API 。
了解了这些,我们就可以开始实现图书网站的功能了。但首先我们来使用简单的Hello World网页简短地介绍下基本网站概念。
要开始了解 Odoo 网页开发的基础,我们将先实现一个Hello World网页来展示基本概念和技术。很有想象空间,是不是?
要创建第一个网页,我们需要一个控制器对象。首先来添加controllers/hello.py文件:
1、在library_website/__init__.py文件中添加如下行:
1 |
from . import controllers |
2、在library_website/controllers/__init__.py文件中添加如下行:
1 |
from . import hello |
3、添加实际的控制器文件 library_website/controllers/hello.py,代码如下:
1 2 3 4 5 6 |
from odoo import http class Hello(http.Controller): @http.route('/helloworld', auth="public") def helloworld(self): return(' Hello World!') |
odoo.http模块提供 Odoo 网页相关的功能。我们用于渲染页面的控制器,应该是一个继承了odoo.http.Controller类的对象。实际使用的名称并不是太重要,这里选择了 Hello(),一个常用的选择是 Main()。
在控制器类中使用了匹配 URL 路由的方法。这些路由用于做一些处理并返回结果,通常是返回用户网页浏览器的 HTML 页面。odoo.http.route装饰器用于为 URL 路由绑定方法,本例中使用的是/helloworld 路由。
安装library_website模块(~/odoo-dev/odoo/odoo-bin -d dev12 -i library_website)就可以在浏览器中打开http://xxx:8069/helloworld,我们应该就可以看到Hello World问候语了。
本例中方法执行的处理非常简单,它返回一个带有 HTML 标记的文本字符串,Hello World。
ℹ️使用这里的简单 URL 访问按制器,如果同一 Odoo 实例有多个数据库时,在没有指定目标数据库的情况下将会失败。这可通过在启动配置中设置-d或–db-filter来解决,参见第二章 Odoo 12开发之开发环境准备。
你可能注意到在路由装饰中使用了auth=’public’参数,对于无需登录的用户开放的页面就需要使用它。如果删除该参数,仅有登录用户方可浏览此页面。如果没有活跃会话(session)则会进入登录页面。
小贴士:auth=’public’参数实际表示如果访客未登录则使用public特殊用户运行网页控制器。如果登录了,则使用登录用户来代替public。
使用 Python 字符串来创建 HTML 很快就会觉得乏味。QWeb可用来增添色彩,下面就使用模板来写一个改进版的Hello World网页。QWeb模板通过 XML 数据文件添加,技术层面上它是与表单、列表视图类似的一种视图类型。它们甚至存储在同一个技术模型ir.ui.view中。
老规矩,需要在声明文件中添加声明来加载文件,编辑library_website/__manifest__.py文件并添加内容如下:
1 2 3 4 |
'data': [ ... 'views/helloworld_template.xml', ], |
然后添加实际的数据文件views/helloworld_template.xml,内容如下:
1 2 3 4 5 6 |
Hello again World!
|
实际上是一种简写形式,它声明
1 2 3 4 5 6 7 8 |
from odoo import http from odoo.http import request class Hello(http.Controller): @http.route('/helloworld', auth="public") def helloworld(self, **kwargs): return request.render('library_website.helloworld') |
模板的渲染是通过render()函数的 request 对象来实现的。
小贴士:注意我们添加了**kwargs方法参数。使用该参数,HTTP 请求中的任意附加参数,如GET 或 POST 请求参数,可通过 kwargs 字典捕获。这会让我们的方法更加健壮,因为即便添加了未预期的参数也不会产生错误。
下面我们来增加点趣味性,创建我们自己的简单 CMS。为此我们可以通过 URL在路由中使用模板名(一个页面),然后对其进行渲染。然后就可以动态创建网页,通过我们的 CMS 来提供服务。实现方法很简单:
1 2 3 |
@http.route('/hellocms/ def hello(self, page, **kwargs): return http.request.render(page) |
以上page 参数应匹配一个模板的外部ID,如果在浏览器中打开http://xxx:8069/hellocms/library_website.helloworld,应该又可以看到熟悉的Hello World 页面了。实际上内置的website模块提供了CMS功能,在 /page路径(endpoint)下还包含更为健壮的实现。
ℹ️在werkzeug的行话中,endpoint是路由的别名,由其静态部分(不含占位符)来表示。比如,CMS 示例中的 endpoint为/hellocms。
大多数情况下,我们要将页面集成到 Odoo 网站中,因此接下来的示例将使用website插件模块。
前面的示例并未集成到 Odoo 网站中,并有页面 footer 和网站菜单。Odoo 的website插件模板为方便大家提供这些功能。
要使用网站功能,我们需要在工作实例中安装website插件模块。应当在library_website插件模块中添加这一依赖,修改__manifest__.py的 depends 内容如下:
1 2 3 4 |
'depends': [ 'library_checkout', 'website', ], |
要使用网站功能,我们需要对控制器和 QWeb模板进行一些修改。控制器中可在路由上添加一个额外的website=True参数:
1 2 3 |
@http.route('/helloworld', auth="public", website=True) def helloworld(self, **kwargs): return request.render('library_website.helloworld') |
集成website模块并非严格要求website=True参数,不添加它也可以在模板视图中添加网站布局。但是通过添加可以让我们在网页控制器中使用一些功能:
如果在网页控制器中无需使用上述功能,则可省略website=True参数。但大多数网站QWeb模板需要使用website=True开启一些数据,比如底部公司信息,所以最好还是添加上。
ℹ️传入QWeb运行上下文语言的网站数据由website/model/ir_ui_view.py文件中的_prepare_qcontext方法设定。
要在模板中添加网站的基本布局,应为QWeb/HTML包裹一个t-call=”website.layout”指令,如下所示:
1 2 3 4 5 |
Hello World!
|
t-call运行QWeb模板website.layout并向其传递 XML 内的tcall 节点。website.layout设计用于渲染带有菜单、头部和底部的完整网页,交将传入的内容放在对应的主区域内。这样,我们的Hello World!示例内容就会显示在 Odoo 网站页面中了。
我们的网站页面可能需要一些其它的 CSS 或JavaScript资源。这方面的网页由website 管理,因此需要一个方式来告诉它使用这些文件。我们将使用 CSS 来添加一个简单的删除线效果,创建library_website/static/src/css/library.css文件并添加如下内容:
1 2 3 |
.text-strikeout { text-decoration: line-through; } |
接下来需要在网站页面中包含该文件。通过在website.assets_frontend模板中添加来实现,该模板用于加载网站相关的资源。添加library_website/views/website_assets.xml数据文件来继承该模板:
1 2 3 4 5 6 7 8 9 10 11 |
name="library_website_assets" inherit_id="website.assets_frontend"> href="/library_website/static/src/css/library.css" /> |
很快我们就会使用text-strikeout这个新的样式类。当然,可以使用相似的方法来添加JavaScript资源。
既然我们已经过了一遍基础知识,就来一起实现借阅列表吧。我们需要使用/checkout URL来显示借阅列表的网页。为此我们需要一个控制器方法来准备要展示的数据,以及一个QWeb模板来向用户进行展示。
在模块中添加library_website/controllers/main.py文件,代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
from odoo import http from odoo.http import request class Main(http.Controller): @http.route('/checkouts', auth='user', website=True) def checkouts(self, **kwargs): Checkout = request.env['library.checkout'] checkouts = Checkout.search([]) return request.render( 'library_website.index', {'docs': checkouts}) |
控制器获取要使用的数据并传给渲染的模板。本例中控制器需要一个登录了的会话,因为路由中有一个auth=’user’属性。这是默认行为,推荐明确指出需要用户会话。登录了的用户存储在环境对象中,通过 request.env来使用。search()语句使用它来过滤出相应的借阅记录。
对于无需登录即可访问的控制器,所能读取的数据也是非常有限的。这种情况下,我们经常需要对部分代码采用提权上下文运行。这时我们可使用sudo()模型方法,它将权限上下文权限修改为内部超级用户,突破大部分限制。权力越大,责任越大,我们要小心这种操作带来的安全风险。需要特别注意在提权时输入的参数以及执行的操作的有效性。建议将sudo() 记录集操作控制在最小范围内。
回到我们的代码,它以request.render()方法收尾。和之前一样,我们传入了QWeb模板渲染的标识符,和模板运行用到的上下文字典。本例中我们向模板传入 docs 变量,该变量包含要渲染借阅记录的记录集。
QWeb模板使用数据文件来添加,我们可以使用library_website/views/checkout_template.xml文件并添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Checkouts
t-att-checked="'checked' if doc.stage_id.fold else None" />
t-att-class="'text-strikeout' if doc.stage_id.fold else ''" />
|
以上代码使用t-foreach指令来迭代 docs 记录集。我们使用了复选框 input 并在借阅完成时保持为已选状态。在 HTML 中,复选框是否被勾选取决于是否有 checked 属性。为此我们使用了t-att-NAME指定来根据表达式动态渲染 checked 属性。当表达式运行结果为 None(或任意其它 false 值)时,QWeb会忽略该属性,本例用它就非常方便了。
在渲染任务名时,t-attf指令用于动态创建打开每个指定任务的明细表单的URL。我们使用一个特殊函数slug()来为每条记录生成易于阅读的 URL。该链接目前尚无法使用,因为我们还没有创建对应的控制器。
在每条借阅记录上,我们还使用了t-att 指令来在借阅为最终状态时应用text-strikeout样式。
借阅列表中的每一项都有一个相应明细页面的链接。我们就为这些链接实现一个控制器,以及实现一个QWeb模板来用于展示。说到这里应该已经很明朗了。
在library_website/controllers/main.py文件中添加如下方法:
1 2 3 4 5 6 7 8 9 10 |
class Main(http.Controller): ... @http.route('/checkout/ auth='user', # 默认值,但此处明确指定 website=True) def checkout(self, doc, **kwargs): return http.request.render( 'library_website.checkout', {'doc': doc}) |
注意这里路由使用了带有model(“library.checkout”)转换器的占位符,会映射到方法的 doc 变量中。它从 URL 中捕获借阅标识符,可以是简单的 ID 数值或链接别名,然后转换成相应的浏览记录对象。
对于QWeb模板,应在library_website/views/checkout_template.xml数据文件中添加如下代码:
1 2 3 4 5 6 7 8 9 |
Member:
Stage:
|
这里值得一提的是使用了
补充:controllers/__init__.py和__mainfest__.py 中请自行添加控制器文件和数据文件的引用
读者现在应该对网站功能的基础有了不错的掌握。我们学习了如何使用网页控制器和QWeb模板来动态渲染网页。然后学习了如何使用website插件并使用它来创建我们自己页面。最后,我们介绍了网站表单插件来帮助我们来创建网页表单。这些都是创建网站功能的核心能技巧。
我们已经学习了Odoo 主要构件的开发,是时候学习如何将Odoo 服务部署到生产环境了。
☞☞☞第十四章 Odoo 12开发之部署和维护生产实例