创建第一个odoo 应用
Odoo遵循传统的MVC模式。我们可以通过创建简单的To-Do 应用来具体介绍分析
- model :定义了应用的数据结构
- view: 描述了用户界面(可以理解为前端)
- controller: 控制器,支持应用的具体业务逻辑
理解applications(应用)跟modules(模块)的区别:
- Module addons :它是applications的基础,它能为Odoo添加新功能或者改变一个已经存在的模块,它包含了一个名为
__mainfest__.py
的字典文件,和一些能够实现新功能的文件 - Applications : 它是将主要功能添加到Odoo的一种方式.例如Odoo中的Accounting or HR,依赖与之相对应对Applications,提供了非常重要的功能。它们在Odoo中的Apps菜单中高亮显示 简而言之,当你的module十分复杂,为Odoo添加了新的或者非常重要的功能,你可能就需要把它创建为一个Application。当你的module只是为已经存在的Odoo模块增加小变动。就不需要作为一个Application
创建一个module基础架构
我们把新的module放入在custom-addons文件夹中,新建todo_app文件夹,然后在该文件夹中创建
__manifest__.py
文件,由于todo_app是python包,需要新建__init__.py
.mkdir -p custom-addons/todo_app
cd custom-addons/todo_app
touch __init__.py
vim __manifest__.py
在__manifest__.py
中添加如下代码{ 'name': 'To-Do Application', 'description': 'Manage your personal task', 'author': 'xer', 'depends': ['base'], 'application': True, 'data': [ ], }
以上
depende
这个key对应了一个list用来存放所依赖的模块。例子中的‘base‘ 表示我们新建的todo_app在安装时候会自动把Odoo中的’base’一起安装.list中存放的都是模块的包名,就跟’todo_app’这样.其他
__manifest__.py
中的keys:- summary:module的次要标题
- version: 代码module的版本号
- license:版权,默认LGPL-3
- website:设置一个URL用来查找模块相应的信息
- category:module的分类,默认为Uncategorized。目前已存在的categories可以在Settings| User | Groups中找到
- installable: 默认为True
- auto_install: 如果设置为True 这个模块会自动安装.
添加todo_app到addons path
- 我们首选需要保证addons path中有我们刚才新建的todo_app的文件路径
./odoo-bin -d todo --addons-path='custom-addons,odoo/addons' --save
安装todo_app
- Apps ==>Update Apps List 然后在搜索框搜索’todo_app’ 点击安装即可
更新module
当我们的Python 代码发生改变时,需要对Odoo进行重启, 因为odoo在启动时只对当前python代码读取一次
当我们的date files更改时候(例如views中添加新的视图) 这时需要在Apps中对module进行upgrading操作.
当python 代码跟data files同时改变时.要重启并upgrading module.
在这里其实有个更好的方法: 首先 Ctrl+c停止Odoo服务后,命令行输入
odoo-bin -d todo -u todo_app
这里运用了参数-u <需要更新的模块名(list)>
来指定需要更新的模块名.
服务端开发者模式
在odoo10 中有一个新的参数在开启Odoo服务时可以运用
--dev=all
这个参数有利于加快我们的开发:
- 自动的重载python代码.
- 自动的读取xml文件中的新定义.避免手动更新
model 层:
Models 描述了业务对象,例如 sales order, partner等,一个model 有许多属性(attributes) ,并能在其中定义特殊的业务逻辑。
Models 使用Python(目前仍为2.7)语言进行编写。使用了ORM模式可以对数据库直接进行操作。
我们会在 todo_app 模块中新建一个to-do tasks model来对Models进行一步步的深入.这个task会有一个text field 用来描述task的详细情况,还有一个选择框来标记task是否被完成.最后会添加一个按钮(button)来清除那些旧的,已经被完成的tasks.
创建一个data 模块:
根据Odoo的常规做法,我们需要一个models文件夹来存放用python编写的models.py文件。但在这个简单的例子中,我们不遵循这个规范,直接把todo_model.py放在todo_app文件夹中
在todo_model.py 中添加相应的python代码:
# -*- coding: utf-8 -*- from odoo import models, fields class TodoTask(models.Model): _name = 'todo.task' _description = "To-do Task" name = fields.Char('Description', required=True) is_done = fields.Boolean('Done') active = fields.Boolean('Active?', default=True)
注意点:
- 需要从odoo中导入models,fields 对象
- 创建一个TodoTask类,该类继承自models.Model类
- _name属性:用来定义我们新添加的这个类的名字.可以看做是当Odoo需要关联TodoTask类时的标识符。
- _description属性:这个属性不是必须的。用来描models的记录。
- 最后三行内容具体描述了我们的model的具体字段,其中name跟active是特殊字段:
- name:用作于记录的名称。(在与其他models有关联关系时候,如Many2one,Many2many时显示在关联models上的名称。)。
- active:只有当记录中的active字段为true时,该记录才能在页面视图中显示。
最后,在
__init__.py
中添加如下代码from . import todo_model
整体结构图--custom-addons ----todo_app -------__manifest__.py -------__init__.py -------todo_models.py
这里扩展下,’_rec_name’属性:默认关联显示名称为name对应的fields.但’_rec_name’属性可以根据需要把要显示在关联models上的名字定义为想要的fields的名字.
重启Odoo服务,由于我们还没定义菜单,需要进入Settings | Technical | Database Structure | Models.使用’todo.task’来搜索我们刚才定义的model.点击查询结果来查看.
Odoo会在创建model数据表时自动加入4个字段
- id: 可认为是每个model record的主键
- create_date 和create_uid :什么时候创建,哪个用户创建
- write_date and write_uid: 确认最后的修改时间和修改的用户
__last_update
:没有实际存贮在数据库中,使用在并发检查上
添加自动测试
- Odoo有2种测试方法:
- YAML
- Python的测试类(Unittest2).
下面主要介绍第二种测试方法:
test代码名字需要用’test_‘作为开头,并且需要在
tests/__init__.py
导入.但是tests文件夹不需要在modules中的__init__.py
导入.因为Odoo会自己搜索测试代码.在todo_app文件夹中新建tests文件夹,在tests中新建
__init__.py
.__init__.py
中添加代码:from . import test_todo
创建test_todo.py文件,添加代码
from odoo.tests.common import TransactionCase class TestTodo(TransactionCase): def test_create(self): 'Create a simple Todo' Todo = self.env['todo.task'] task = Todo.create({'name': 'Test Task'}) self.assertEqual(task.is_done, False)
运行测试代码
./odoo-bin -d todo -i todo_app --test-enable
view层
view可以认为是用户跟Odoo内部数据直接进行联系的一个交互界面(通过html前端页面来展示数据,通过form表单来提交用户输入数据到Odoo数据库)
Odoo中的Views是通过xml来编写的。Odoo中的xml 文件一般都存放在views这个子文件夹中。我们首先来进行对menu items进行编写。(menu可以看做是一个动作,点击后能渲染视图在前端显示)
添加 menu 主题
既然我们已经有todo_models来存放我们的数据,那现在我们就需要让用户能够通过menu来与之交互.
创建’views/todo_menu.xml’ 来定义menu.添加如下xml 代码
<?xml version="1.0"?> <odoo> <act_window id="action_todo_task" name="To-do Task" res_model="todo.task" view_mode="tree,form"/> <!--menuitem --> <menuitem id="menu_todo_task" name="Todos" action="action_todo_task"/> </odoo>
The user interface,包括menu 跟actions 都是存储在数据库表中的。而我们的xml 文件可以看做用来把这些数据库中的表展示出来在用户界面上体现。前面的代码描述添加到Odoo中的2条records。
<act_window>
:定义了客户端的窗口动作,会通过tree视图跟form视图来打开我的定义的todo.task<menuitem>
:定义了顶部的(menu)菜单栏,该菜单栏绑定了一个名为’action_todo_task’的action.
所有的元素中包含id 这个属性, 这个id属性十分重要,被称为XML ID:它用于唯一标识模块内部的每个数据元素,并且能够被其他元素所引用到.在我们的例子中,
<menuitem>
元素需要与action联系起来,所以需要把<act_window>
的id关联到<menuitem>
的action中添加xml文件到
__manifest__.py
:'data' : ['views/todo_menu.xml'],
更新todo_app模块,发现我们的菜单栏中已经有了’Todos’这个菜单选项
添加一个form视图
所有的视图都存储在数据库中,在’ir.ui.view’这个model中.添加一个view到module,我们在XML文件中声明
<record>
这个元素,当模块被安装后这个<record>
中的数据就被加载进数据库中。添加views/todo_view.xml文件,添加如下代码
<?xml version="1.0" encoding="UTF-8" ?> <odoo> <!--<form view>--> <record id="view_form_todo_task" model="ir.ui.view"> <field name="name">To-do Task Form</field> <field name="model">todo.task</field> <field name="arch" type="xml"> <form string="To-do Task"> <group> <field name="name"/> <field name="is_done"/> <field name="active" readonly="1"/> </group> </form> </field> </record> </odoo>
之后别忘记添加到
__manifest__.py
中的’data’中.这个’todo_view.xml’文件在’ir.ui.view’ model中添加了一条标识为’view_form_todo_task’的记录. 注意:这边的话其实添加的所谓标识是添加在’ir.model.data’ 表中作为一个外部ID来与’ir.ui.view’进行mapping关系的建立.而不是直接添加到’ir.ui.view’表中可以通过psql查询数据库
select name from ir_ui_view where name='To-do Task Form'
来得到.(注意,在psql中,所有Odoo的数据表名称的.
都用_
代替了.因为’view_form_todo_task’是添加在’ir.model.data’中,所以查询’ir_ui_view’无法得到外部ID).在上述
<record>
代码中,最为重要的属性是’arch’.它包含了需要的视图,例子中就定义了视图为<form>
.接下来的三段定义是把我们todo_models中的fields呈现在form视图中.其中的’active’字段属性我们设置为了只读
业务文档form视图
- 对于文档模型,Odoo有一个专门的模仿成一页纸形的展示风格。这个视图包含了2个元素:
<header>
可以在其中添加<button>
按钮<sheet>
(让form好看一点) >
添加动作按钮(action buttons)
form表单中可以定义buttons来执行相应的actions,这些buttons可以能够打开一个新的窗口或者运行一个定义在models中的python方法.
buttons能定义在form中任何地方, 但是对于文档格式的forms来说,建议存放在
中 对于我们的todo_app应用,我们添加2个buttons来运行我们定义在’todo.task’ model中的2个方法
<header> <button name="do_toggle_done" type="object" string="Toggle Done" class="oe_highlight"/> <button name="do_clear_done" type="object" string="Clear All Done"/> </header>
我们定义的buttons包含了下面4个属性 1.
string
:显示为button在页面上的名字 2.type
: 动作的类型(这里的object
可以理解为调用了我们todo_model.py中的TodoTask
这个类创建的object) 3.name
: action的标识符(我们代码中的name可以理解为在TodoTask
中定义的do_toggle_done
方法. 4.class
:这是一个可选选项来运用CSS样式.(我们的oe_highlight
表示为把这个button设置为高亮显示)
使用<group>
来组织form视图
<group>
标签可以让我们把form中的内容组织起来。可以嵌套使用,就像<group><group>....</group></group>
这样,能够在外部的<group>
的内容中增加两个纵列.其实可以这样理解,一个<group>
就是把我们field的字段分为左右两列,左边是field的名字,右边是field要输入的我们建议在
<group>
元素中添加一个name
属性以便以后更好的扩展我们的group<sheet> <group name="top"> <group name="left"> <field name="name"/> </group> <group name="right"> <field name="is_done"/> <field name="active" readonly="1"/> </group> </group> </sheet>
最后,我们的form视图代码如下:
<form string="To-do Task"> <header> <button name="do_toggle_done" type="object" string="Toggle Done" class="oe_highlight"/> <button name="do_clear_done" type="object" string="Clear All Done"/> </header> <sheet> <group name="top"> <group name="left"> <field name="name"/> </group> <group name="right"> <field name="is_done"/> <field name="active" readonly="1"/> </group> </group> </sheet>> </form>
展示效果:
添加列举(list)跟搜索(search)视图
当需要列举一个model中的数据时,我们就需要使用
视图。Tree视图能够展示结构化的层级关系,(如linux下的tree命令)在Odoo中,我们通常使用Tree视图来展示清晰的数据列表。 我们在todo_view.xml中添加如下的tree视图定义代码:
<!--tree view--> <record id="view_tree_todo_task" model="ir.ui.view"> <field name="name">To-do Task tree</field> <field name="model">todo.task</field> <field name="arch" type="xml"> <tree colors="decoration-muted:is_done==True"> <field name="name"/> <field name="is_done"/> </tree> </field> </record>
以上的定义主要在列举数据时只显示两列
name
跟is_done
.我们在这里设置了一个bootstrap的CSS样式,当task的记录中is_done
是True
时,记录显示为灰色.(需要安装Odoo的’website’模块才能使用bootstrap).Odoo的右上角有一个搜索框,我们可以定义一个
视图来为这个搜索框添加可选的过滤条件。 下面构造我们的
视图代码: <!--search view--> <record id="view_filter_todo_task" model="ir.ui.view"> <field name="name">To-do Task Filter</field> <field name="model">todo.task</field> <field name="arch" type="xml"> <search> <field name="name"/> <filter string="Not Done" domain="[('is_done','=',False)]"/> <filter string="Done" domain="[('is_done','!=',False)]"/> </search> </field> </record>
可以看到在
标签中,可以定义 这个标签,使用 domain
属性来实现过滤条件.
逻辑层
- 现在我们需要添加业务逻辑来实现我们的buttons:使用python在我们的todo_models.py中编写与业务相关的python methods.
添加业务逻辑
编辑我们在models文件夹中的’todo_model.py’文件.首先,我们需要import 新的API
from odoo import models, fields, api
我们的Toggle Done按钮逻辑十分简单,就是用来转换我们创立的task中的’Is_Done’这个flag.我们使用
@api.multi
这个装饰器来表示对多条records的逻辑修改.self
代表一个recordset.我们可以通过遍历self
来达到遍历recordset从来得到需要的每条record.在TodoTask 类中,添加如下python代码
@api.multi def do_toggle_done(self): for task in self: task.is_done = not task.is_done return True
上面的代码实现的逻辑是:遍历所有的’to-do task’ 记录,然后修改每条记录的’is_done’ field, 反转它的值.在最后我们return True是因为Odoo客户端使用XML-RPC来调用我们的’do_toggle_done’方法,而这个协议不支持客户端方法返回None值。
Clear All Done方法: 找到所有’is_done’的值为True的记录,把它们的active属性设置为False以达到让该条记录不在页面显示的功能.通常来说,放在form视图中的button都是用来操作当前选中的记录,在我们的这个例子中,我们想要让这个button能够影响所有的records.
@api.model def do_clear_done(self): dones = self.search([('is_done', '=',True)]) dones.write({'active': False}) return True
上面的代码中.
@api.model
装饰器装饰的方法中,self
变量代表了当前的model,我们把所有is_done = True
的recordset存放在变量dones
中.然后再把它们的active
字段设置为False
.search
方法是Odoo提供的一个API方法,返回符合条件的records. 这些条件使用Odoo的domain规则.是一个tuple组成的list.write
方法能够对recordset直接使用.传入的参数是一个’dict’.
添加测试
我们需要为我们的业务逻辑添加测试,在我们以前编写的测试类’tests/test_todo.py’ ,添加如下测试方法 test_create()
def test_create(self): task.do_toggle_done() self.assertTrue(task.is_done) Todo.do_clear_done() self.assertFalse(task.active)
使用
./odoo-bin -d todo -i todo_app --test-enable
设置模块访问权限(access security)
当我们加载我们的模块时,记录中会有一条错误信息: The model todo.task has no access rules, consider adding one.
这条信息说明了我们的todo_app模块没有设置访问权限规则,除了超级管理员以外其他的用户不能使用我们的模块. 另一个问题是我们需要让不同的用户拥有自己私有的to-do tasks.
测试访问权限
实际上,我们在前面编写的测试代码不应该成功,但我们的管理员(admin)用户身份导致了测试类完整运行。现在,我们使用Demo来代替我们的admin用户。
编辑’tests/test_todo.py’文件,我们新添加
set Up
这个方法def setUp(self,*args,**kwargs): result = super(TestTodo, self).setUp(*args, **kwargs) user_demo = self.env.ref('base.user_demo') self.env = self.env(user=user_demo) return result
函数定义的第一行,我们调用了父类中的
set Up
方法.接下来就是对调用测试方法的用户的更改,上述代码中,我们改变了测试环境,把demo用户代替了admin接下来,我们在测试类中导入了断言异常的处理函数.
from odoo.exceptions import AccessError
在test 类中添加一个新的测试方法:
def test_record_rule(self): 'Test per user record rules' Todo = self.env['todo.task'] task = Todo.sudo().create({'name': 'Admin Task'}) with self.assertRaises(AccessError): Todo.browse([task.id]).name
因为我们现在的
env
已经使用Demo用户,我们使用sudo()方法(可以理解为linux下的sudo)来切换到admin的上下文环境创立一个name为Admin Task的task。这么做的目的是创建一个不能被Demo所获取的to-do task.当我们尝试使用Demo用户去获取上文创建的task时,会有一个AccessError异常被抛出.
注意: 此时,当我们尝试运行刚才编写的test时,会发生错误,因为我们还没有设置权限设置.
添加访问控制
- 访问Settings | Technical | Security| Access Controls List:
这些信息是有模块提供然后加载到odoo中的’ir.model.access’模块,接下来,我们就在我们的model中添加所有权限给员工(employee)这个分组.注:员工这个分组是非常普通的.
我们可以通过使用CSV文件来设置权限.新建’security/ir.model.access.csv’文件,添加如下内容
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_todo_task_group_user,todo.task.user,model_todo_task,base.group_user,1,1,1,1
我们定义的CSV文件中,第一行的那些名字可以看做是书写的参照规则:
id :外部标识,在我们的模块中应该是独一无二的
name :是一个描述标题,作为一个信息的展示,最好能保持唯一性.Odoo官方模块通常使用
modelname.group
来进行取名.我们的todo_app中,使用了’todo.task.user’model_id: 这是我们要赋予权限的model的外部标识,通常来说,Odoo会通过ORM对models使用自动的取名.对于我们的todo.task来说,这个外面id是’model_todo_task’.
group_id: 标识了需要赋予权限的用户组.Odoo中重要的用户组一般都是base模块提供的,而我们需要的Employee的id为base.group_user
perm: 4个,‘read’,‘write’,‘create’,‘unlink’.分别代表了对读,写,创建,删除的4个操作的允许与否. 创建了security/ir.model.access.csv文件后还需要把其路径导入到
__manifest__.py
的’data’列表中'data':[ 'security/ir.model.access.csv', ... ],
现在,更新我们的模块,warning message就会消失了,我们登录Demo账户也能发现能够访问我们的todo_app模块。运行测试类时只有’test_record_rule’方法仍会失败
Row-level的访问规则
Setting | Technical | Record Rules 记录规则被定义在’ir.rule’ model中,跟往常一样,我们需要提供一个特殊的名字,记录规则作用在的具体的model以及需要定义的domain过滤规则来进行权限约束。
通常,规则被适用于特殊的安全用户组,在我们的例子中,我们还是会使用员工(Employees)用户组,如果不定义规则要适用的用户组,Odoo就会认为是一个全局规则(Global rules).全局规则是特殊的存在因为它们无法被普通的规则覆盖。
为了添加记录规则,我们创建’security/todo_access_rules.xml’文件,添加如下代码
<?xml version="1.0" encoding="utf-8" ?> <odoo> <data noupdate="1"> <record id="todo_task_user_rule" model="ir.rule"> <field name="name">ToDo_rule</field> <field name="model_id" ref="model_todo_task"/> <field name="domain_force">[('create_uid','=',user.id)]</field> <field name="groups" eval="[(4,ref('base.group_user'))]"/> </record> </data> </odoo>
上面代码中,注意
noupdate='1'
属性,这个属性意味着当模块被升级时,我们的data并不会发生改变.通常在开发时我们需要多次调整数据,所以可以把它的值设为’0’.- 在
字段中,我们发现有一个特殊的表达式,这是一个一对多的关系字段,在我们的例子中(4,x)这个tuple 表明把x添加到记录中,在这里,x代表了员工用户组,验证id为 base.group_user. - 最后别忘记把我们刚新建的xml文件放入
__manifest__.py
的’data’中. - 下面,我们来运行我们编写的测试类,可以发现全部 测试通过.
- 在
添加一个module的图标
添加一个icon图标让我们的module看起来好看点. 把它放入module目录下的static/description目录中即可. 注意名字命名为icon
在todo_app目录路径下,简单的几句linux代码:
mkdir -p static/description cp 我们自己的icon图标 static/description