Odoo入门(二)——创建todo-app

创建第一个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:

    1. summary:module的次要标题
    2. version: 代码module的版本号
    3. license:版权,默认LGPL-3
    4. website:设置一个URL用来查找模块相应的信息
    5. category:module的分类,默认为Uncategorized。目前已存在的categories可以在Settings| User | Groups中找到
    6. installable: 默认为True
    7. 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' 点击安装即可

image.png

更新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

  • 这个参数有利于加快我们的开发:

    1. 自动的重载python代码.
    2. 自动的读取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的具体字段,其中nameactive是特殊字段:
      • 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.点击查询结果来查看. image.png

  • Odoo会在创建model数据表时自动加入4个字段

    1. id: 可认为是每个model record的主键
    2. create_date 和create_uid :什么时候创建,哪个用户创建
    3. write_date and write_uid: 确认最后的修改时间和修改的用户
    4. __last_update:没有实际存贮在数据库中,使用在并发检查上

添加自动测试

  • Odoo有2种测试方法:
    1. YAML
    2. 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’这个菜单选项 image.png

添加一个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好看一点)

    image.png

添加动作按钮(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要输入的 image.png

  • 我们建议在<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>
  • 展示效果: image.png

添加列举(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>
  • 以上的定义主要在列举数据时只显示两列nameis_done.我们在这里设置了一个bootstrap的CSS样式,当task的记录中is_doneTrue时,记录显示为灰色.(需要安装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
  1. 因为我们现在的env 已经使用Demo用户,我们使用sudo()方法(可以理解为linux下的sudo)来切换到admin的上下文环境创立一个name为Admin Task的task。这么做的目的是创建一个不能被Demo所获取的to-do task.

  2. 当我们尝试使用Demo用户去获取上文创建的task时,会有一个AccessError异常被抛出.

  • 注意: 此时,当我们尝试运行刚才编写的test时,会发生错误,因为我们还没有设置权限设置.

添加访问控制

  • 访问Settings | Technical | Security| Access Controls List: image.png

  • 这些信息是有模块提供然后加载到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文件中,第一行的那些名字可以看做是书写的参照规则:
    1. id :外部标识,在我们的模块中应该是独一无二的

    2. name :是一个描述标题,作为一个信息的展示,最好能保持唯一性.Odoo官方模块通常使用modelname.group来进行取名.我们的todo_app中,使用了’todo.task.user'

    3. model_id: 这是我们要赋予权限的model的外部标识,通常来说,Odoo会通过ORM对models使用自动的取名.对于我们的todo.task来说,这个外面id是’model_todo_task'.

    4. group_id: 标识了需要赋予权限的用户组.Odoo中重要的用户组一般都是base模块提供的,而我们需要的Employee的id为base.group_user

    5. 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