创建第一个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': [ ],
}
- 以上depends这个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
这个参数有利于加快我们的开发:
- 自动的Reload python代码.
- 自动的读取xml files中的新定义.避免手动更新
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 代码
- The user interface,包括menu 跟actions 都是存储在数据库表中的。而我们的xml 文件可以看做用来把这些数据库中的表展示出来在用户界面上体现。前面的代码描述添加到Odoo中的2条records。
-
:定义了客户端的窗口动作,会通过tree视图跟form视图来打开我的定义的todo.task - :定义了顶部的(menu)菜单栏,该菜单栏绑定了一个名为'action_todo_task'的action.
-
- 所有的元素中包含id 这个属性, 这个id属性十分重要,被称为XML ID:它用于唯一标识模块内部的每个数据元素,并且能够被其他元素所引用到.在我们的例子中,
- 添加xml文件到'manifest.py':
'data' : ['views/todo_menu.xml'],
更新todo_app模块,发现我们的菜单栏中已经有了'Todos'这个菜单选项
提升我们的视图
添加一个form视图
- 所有的视图都存储在数据库中,在'ir.ui.view'这个model中.添加一个view到module,我们在XML文件中声明
这个元素,当模块被安装后这个 中的数据就被加载进数据库中。 - 添加views/todo_view.xml文件,添加如下代码
To-do Task Form
todo.task
之后别忘记添加到'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).在上述
代码中,最为重要的属性是'arch'.它包含了需要的视图,例子中就定义了视图为 接下来的三段定义是把我们todo_models中的fields呈现在form视图中.其中的'active'字段属性我们设置为了只读
业务文档form视图
- 对于文档模型,Odoo有一个专门的模仿成一页纸形的展示风格。这个视图包含了2个元素:
:可以在其中添加 : (让form好看一点)
添加动作按钮(action buttons)
- form表单中可以定义buttons来执行相应的actions,这些buttons可以能够打开一个新的窗口或者运行一个定义在models中的python方法.
- buttons能定义在form中任何地方, 但是对于文档格式的forms来说,建议存放在
中 - 对于我们的todo_app应用,我们添加2个buttons来运行我们定义在'todo.task' model中的2个方法
- 我们定义的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设置为高亮显示)
使用来组织form视图
标签可以让我们把form中的内容组织起来。在一个 中在放入 ,就像
这样,能够在外部的的内容中增加两个纵列(其实可以这样理解,一个 就是把我们field的字段分为左右两列,左边是field的名字,右边是field要输入的.)
- 我们建议在
元素中添加一个 name
属性以便以后更好的扩展我们的
最后,我们的form视图代码如下:
展示效果:
添加列举(list)跟搜索(search)视图
- 当需要列举一个model中的数据时,我们就需要使用
视图。Tree视图能够展示结构化的层级关系,(如linux下的tree命令)在Odoo中,我们通常使用Tree视图来展示清晰的数据列表。 - 我们在todo_view.xml中添加如下的tree视图定义代码:
To-do Task tree
todo.task
- 以上的定义主要在列举数据时只显示两列
name
跟is_done
.我们在这里设置了一个bootstrap的CSS样式,当task的记录中is_done
是True
时,记录显示为灰色.(需要安装Odoo的'website'模块才能使用bootstrap). - Odoo的右上角有一个搜索框,我们可以定义一个
视图来为这个搜索框添加可选的过滤条件。 - 下面构造我们的
视图代码:
To-do Task Filter
todo.task
- 可以看到在
标签中,可以定义 这个标签,使用 domain
属性来实现过滤条件.
逻辑层
- 现在我们需要添加业务逻辑来实现我们的buttons:使用python在我们的todo_models.py中编写与业务相关的python methods.
添加业务逻辑
- 编辑我们在models文件夹中的'todo_model.py'文件.首先,我们需要import 新的API
from odoo import models, fields, api
- 我们的Toggle Donebutton逻辑十分简单,就是用来转换我们创立的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):
# Test Toggle Done
task.do_toggle_done()
self.assertTrue(task.is_done)
# Test Clear Done
Todo.do_clear_done()
self.assertFalse(task.active)
使用./odoo-bin -d todo -i todo_app --test-enable
设置模块访问权限(access security)
当我们加载我们的模块时,记录中会有一条warning message:
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文件中的列名:
我们定义的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'文件,添加如下代码
ToDo_rule
[('create_uid','=',user.id)]
- 上面代码中,注意
noupdate='1'
属性,这个属性意味着当模块被升级时,我们的data并不会发生改变.通常在开发时我们需要多次调整数据,所以可以把它的值设为'0'. - 在
字段中,我们发现有一个特殊的表达式,这是一个一对多的关系字段,在我们的例子中(4,x)这个tuple 表明把x添加到记录中,在这里,x代表了员工用户组,验证id为 base.group_user. - 最后别忘记把我们刚新建的xml文件放入manifest.py的'data'中.
- 下面,我们来运行我们编写的测试类,可以发现全部
测试通过.
添加一个module的图标
- 添加一个icon图标让我们的module看起来好看点.
把它放入module目录下的static/description目录中即可.
在todo_app目录路径下,简单的几句linux代码:
mkdir -p static/description
cp 我们自己的icon图标 static/description