Odoo入门(三)——继承机制,扩展todo应用

继承-扩展原有的应用

  • Odoo中,有一个非常重要的特色,不用直接修改底层对象就能为我们的模块添加新的功能。这个特色就是Odoo中的继承(inheritance)机制.继承能够在不同的层面上(models,views,business logic)来进行对原有模块的修改。

为我们的To-Do app增加分享功能

  • 我们的To-Do应用允许用户管理他们自己的to-do 任务,我们接下来需要创建一个新 的模块来扩展前一章的To-Do应用,运用继承机制来添加分享任务和讨论功能。 我们的工作计划如下

    • 添加task的负责人

    • 修改业务逻辑,用户只能修改自己的tasks.

    • 扩展view视图,添加必须的字段

    • 添加社交网络功能:一个信息墙跟关注者。

      效果图

  • 首先根据前一篇文章中创建todo_app时的方法创建todo_user模块的骨架,还是在我们的’custom-addons’模块中新建’todo_user’文件夹。创建’custom-addons/todo_user/manifest.py',添加如下代码

{
    'name': 'Multiuser To-Do',
    'description': 'Extend the To-Do app to multiuser.',
    'author': 'Daniel Reis',
    'depends': ['todo_app'],
}
  • 注意到我们添加了对原先的’todo_app’模块的依赖'depends':['todo_app'].这对我们的继承机制是十分重要的.当我们添加了’depend’后,每次更新我们的’todo_user’模块,它对应的依赖’todo_app’会自动更新.

扩展我们的模型(models)

  • python的classes能够定义新的models.我们对models扩展需要使用Odoo的继承机制来编写classes.

  • 如果要扩展一个已经存在的模型(model),我们使用python的类属性 '_inherit'.它指定了被扩展的模型.通过'_inherit’指定被继承模型后我们新创建的类能获得父类模型的所有功能.因此我们只要对需要修改的地方进行重构即可.

  • 事实上,Odoo的模型存在于我们Python模块的外部,在一个集中的注册处理区,这个注册处理区,能够被我们的模型方法self.env[]所获取到,举例来说,我们可以得到res.partner这个模型的对象通过使用self.env[‘res.partner’]

  • 为了修改Odoo模型,我们获取到这个模型在注册处理区的具体注册类,然后执行修改在这个注册类上。这意味着所有调用该模型的地方都会得到我们修改过后的新的注册类。

  • 还有一个问题需要注意,当我们的Odoo服务启动时,我们的模块加载顺序是密切相关的,所以要必须保障我们的依赖模块是正确的并被顺利加载到addons路径中.

为我们的模型添加新的字段

  • 我们来扩展我们的todo.task模型, 添加2个新的字段:1.task的负责人 2.task的截止日期 创建'/todo_user/models’文件夹,其中创建’todo_task.py’文件,添加以下代码
from odoo import models,fields,api
class TodoTask(models.Model):
    _inherit = 'todo.task'
    user_id = fields.Many2one('res.users', string='Responsible')
    date_deadline = fields.Date('Deadline')
  • 上面的代码中,_inherit作为关键属性:告诉了Odoo我们建立的’TodoTask’这个class是继承并自’todo.task'. 注意点:_name属性在这里没有出现,因为它已经从’todo.task’中被继承了.

  • 最后的两行代码就是最为常见的字段声明.user_id字段代表了来自’res.users’这个模型的用户,它是一个Many2one字段,从数据库角度来说,就是一个外键的作用. date_deadline是一个简单的日期字段.我们更新我们的’todo_user’模块后进入Technical | Database Structure | Models菜单.搜索todo.task模型会发现其中新增了我们刚添加的2个字段.

修改已经存在的字段

  • 就像我们看到的,添加一个新的字段到我们已经存在的模型是相当直接的.从Odoo 8开始,修改模块中已经存在的字段的属性也是可以实现的,做法很简单,添加一个与存在的字段名称相同的字段,然后只修改字段的属性的值就可以了.举例来说,我们添加下列字段到我们的’todo_task.py’中: name = fields.Char(help="What needs to be done?")

  • 这行代码在我们以前定义在’todo.task’模型中的name字段上添加了一个help属性.我们更新我们的’todo_user’模块,把鼠标悬停在’Description’上会有一段help的文字显示.

修改模型的方法

  • 继承机制对业务逻辑层面同样适用,添加新的方法跟添加字段一样简单:直接在class类中添加即可. 跟python的继承类似,当我们适用Odoo的继承机制时,我们在新的继承类中定义与父类相同名字的函数同样可以实现重写.而且在继承类中使用super() 函数来调用父类的方法. 注意点:重写方法时,尽量不要修改传入方法的参数列表,如果实在需要,可以使用关键字参数来实现.

  • 我们的老的’Clear All Done’动作不适合我们现在的共享任务模块,因为这个方法会改变所有用户的任务(task)的’active'.因为我们需要修改这个方法为只能修改自己负责的任务的’active’属性.

    @api.multi
    def do_clear_done(self):
        domain = [('is_done', '=', True),
                  '|', ('user_id', '=', self.env.uid),
                  ('user_id', '=', False)]
        dones = self.search(domain)
        dones.write({'active': False})
        return True
  • 上面的代码很好理解,重写了’do_clear_done’方法,在domain规则中,加入了判断:
  • 任务已经完成and(任务的负责人是当前用户or任务的负责人没有指定) domain 规则是一系列代表判断的tuple的list,中间的逻辑链接and'&',or'|'.写在tuple之前,默认的逻辑是’and'
  • 在上面的代码里,我们完全重写了父类中的方法,但是,我们平时在Odoo中不会这么做.我们应该使用额外的操作代码来扩展父类中的方法,而不是对其原有结构直接更改.所以,我们通常需要使用super()方法来处理. 举个例子:我们需要提高我们的’do_toggle_done()‘方法,让这个方法只能由task的负责人调用
from odoo.exceptions import ValidationError
    @api.multi
    def do_toggle_done(self):
        for r in self:
            if r.user_id != self.env.user:
                raise ValidationError(
                    'Only the responsible can do this!'
                )
        return super(TodoTask, self).do_toggle_done()
  • 上面代码中,我们使用了Odoo自定义的异常类ValidationError.当用户不是task负责人时,用户点击页面上的’Do_Toggle_Done’就会直接把该异常抛出.页面就会弹出一个警告框.

扩展views视图

  • 表单(forms),列表(list),搜索(search)视图都是被’arch’所定义的XML结构.为了扩展视图,我们需要修改这些xml,这就意味着要定位xml元素然后对它们进行修改.

  • 一个继承的视图代码

<record id="view_form_todo_task_inherited" model="ir.ui.view">
	<field name="name">Todo Task form Inherited</field>
	<field name="model">todo.task</field>
	<field name="inherit_id" ref="todo_app.view_form_todo_task"/>
	<field name="arch" type="xml">
	<! -- ..match and extend elements here! ..-->
	</field>
	</record>
`inherit_id`字段定义了需要被继承的视图.通过`ref`属性传入被继承的视图的外部ID(External identifiers)
  • 使用Xpath来定位XML元素是最合适的,举例来说,定位到<field name="is_done">这个元素可以使用表达式//field[@name] = 'is_done'来实现.

  • 如果一个Xpath表达式匹配了多个元素,只有第一个匹配到的元素会被修改,所以最好使用独一无二的元素属性去匹配.通常在Odoo中我们使用name属性去进行匹配,因此,为元素加上name属性是非常重要的.

  • 一旦找到我们需要定位的元素,我们可以修改或者直接添加XML元素来把我们新增的字段放入其中.以我们的继承视图为例,可以把’date_deadline’字段添加在’is_done’前面

<xpath expr="//field[@name]='is_done'" position="before"/>
<field name="date_deadline"/>
</xpath>

幸运的是,Odoo提供了缩写形式,大多数时候,我们可以避免每次都使用这样的表达式,我们可以使用与元素类型相关的信息和它的独特属性来进行定位,上述代码可以改写为:

<field name="is_done" position="before">
     <field name="date_deadline"/>
</field>
  • 注意,当一个字段多次出现在同一个view中,我们还是需要使用Xpath 表达式,因为缩写形式在查找到第一个元素后就停止继续定位了。 position 属性通常有下面的几个值:

    • after 添加到匹配节点的后面

    • before 添加到匹配节点之前

    • inside(默认值) 添加在节点里(一般与之类的一起使用)

    • replace 代替匹配到的节点,如果使用空内容,相当于删除了匹配到的元素。

    • attributes 修改匹配元素的属性。使用<attribute name="attr-name">来进行新属性的设置举例

      <field name="active" position="attributes">
        <attribute name='invisible'>1</attribute>
      </field>
      

这段代码表示把’acitve’这个字段隐藏起来

  • 实际的开发中,我们使用添加’invisible’这个属性来让字段在页面隐藏而尽量避免使用’replace’,因为’replace’会删除我们定位到的节点(有时候这些节点只是作为一个占位符,当replace删除后会改变整个视图的结构)

扩展form视图

  • 编写views/todo_task.xml,

  • 添加的完整的form视图代码如下

        <record id="view_form_todo_task_inherited" model="ir.ui.view">
            <field name="name">Todo Task form Inherited</field>
            <field name="model">todo.task</field>
            <field name="inherit_id"
                 ref="todo_app.view_form_todo_task"/>
            <field name="arch" type="xml">
                <xpath expr="//field[@name='is_done']" position="after">
                    <field name="date_deadline"></field>
                </xpath>
                <xpath expr="//field[@name='name']" position="after">
                    <field name="user_id"></field>
                </xpath>
                <xpath expr="//field[@name='active']" position="attributes">
                    <attribute name="invisible">1</attribute>
                </xpath>
            </field>
        </record>

最后在__manifest__.py中添加todo_task.xml到data属性 'data':['views/todo_task.xml'],

扩展tree跟search视图

  • 与form视图一样,运用inherit_id 这个field来实现继承,在tree视图中,我们添加user_id字段。

     <record id="view_tree_todo_task_inherited" model="ir.ui.view">
        <field name="name">Todo Task tree Inherited</field>
        <field name="model">todo.task</field>
        <field name="inherit_id" ref="todo_app.view_tree_todo_task"/>
        <field name="arch" type="xml">
        <field name="name" position="after">
        <field name="user_id"/>
    		</field>
    	    </field>
    	</record>
    

在search视图我们添加2个新的过滤条件:1.用户自己的任务。2. 那些没有负责人的任务

        <record id="view_filter_todo_task_inherited" model="ir.ui.view">
            <field name="name">Todo Task filter Inherited</field>
            <field name="model">todo.task</field>
            <field name="inherit_id" ref="todo_app.view_filter_todo_task"/>
            <field name="arch" type="xml">
                <field name="name" position="after">
                    <field name="user_id"/>
                    <filter name="filter_my_tasks" string="My Tasks"
                            domain="[('user_id','in',[uid,False])]"/>
                    <filter name="filter_not_assigned" string="Not Assigned"
                            domain="[('user_id','=',False)]"/>
                </field>
            </field>
        </record>

模型继承机制的更多介绍

  • 我们刚才看到的是最为基础的模块扩展,在Odoo官方文档中我们称为class inheritance.这是继承最为频繁的使用,也是最好理解的,即直接对现有模块进行扩展**(in-place extension)**.当我们增加新字段,新功能的同时,这些新功能也被增加到原来已经存在的模型中.并没有创建一个新的模型.

  • 我们能够继承多个父类模型通过使用_inherit属性._inherit能够接收一个列表来置入我们所要继承的父类的名称.通过这种方式,我们能够创建一个混合类**(mixin classes)**。混合类可以看作是那些能够提高基础功能的模型集合。它们不是直接被使用的,而是像一个容器,里面有许多功能,当你想要添加这些功能时就能进行相应的扩展。

  • 假如我们在继承的子模型中使用了不同于父模型的_name,我们就会在数据库中创建一个与父模型功能完全一样的模型,但是这个新模型会有自己的数据库表结构跟数据,官方文档称为原型继承(prototype inheritance)。当你在新的模型中添加功能时,新功能只会添加到新模块中。原来的父类模块并不会发生改变。

  • 还有一种我们称为delegation inheritance的方法(也成实例继承,instance inheritance),允许将模型的每条记录链接到父模型的记录,并且提供对父记录的透明访问。使用了_inherits这个属性

  • 这个继承方式可以这么理解:我们现在需要有一个’teacher’模型跟一个’student’模型,这两个模型都需要被继承自’res.partner’这个模型.我们使用_inherites让我们新创建的两个模型拥有自己的数据库表结构,分别在两个模块中添加新字段或者功能时只会写入对应模块的数据库中,而不会改变’res.partner’模块.

使用prototype inheritance来复制功能

  • 我们在前面的扩展模型只使用了_inherite属性,我们定义了一个模型去继承’todo.tasl’模型,添加了一些功能,但我们没有使用_name.所以继承模块还是使用todo.task的数据库表结构.

  • 现在,我们使用_name属性,这会让我们创建一个新的数据库来复制被继承模型的所用功能.例如:

from odoo import models
class TodoTask(models.Model):
    _name = 'todo.task'
    _inherit = 'mail.thread'
  • 这个例子很好理解,我们通过_inherit把’mail.thread’这个模块的所有信息复制到了’todo.task’模型的数据库表结构中. 复制意味着父模块中的所有方法跟字段都被添加到子模块中.我们的例子里就是把’mail.thread’定义的方法跟字段都添加到了’todo.task'.但是’mail.thread’跟’todo.task’都拥有自己独立的数据库表结构,它们之间没有任何联系.只是共享了fields的定义而已.

通过委托继承(delegation inheritance)植入模型

  • 委托不是那么常用,但是它能提供很多方便的继承解决方法。它使用_inherites属性通过字典映射继承模块与字段之间的关系.一个很好的例子就是我们的标准用户模型,‘res.users’,它植入了一个Partner模型.

    from odoo import models, fields
    class User(models.Model):
        _name = 'res.users'
        _inherits = {'res.partner': 'partner_id'}
        partner_id = fields.Many2one('res.partner')
    
    
    
  • 使用委托继承,‘res.users’模型中植入了继承模型’res.partner’.当一个新的User被创建时,一个新的partner也被创建了,然后通过’partner_id’这个字段把partner关联到了user类中,这与面向对象编程的多态有点类似。

  • 通过委托继承,所有的继承模型中跟Partner中的字段都是可以被获取的因为它们就存在于User的字段中。举例来说,Partner的name跟address字段都显示为User中的字段,实际上它们被存储在相关联的Partner模型中。并不会发生数据重复

  • 相比于原型继承,委托继承的优势在于它不会产生数据重复。当一个新的模型需要添加地址字段时就可以直接使用委托继承来植入Partner模型。而当Partner模型中的地址发生改变时,所有与之有关联的模型中的地址都会改变。 注意,使用委托继承时,字段是可以继承的,但是方法不能。

添加社交网络功能

  • 社交网络模块(mail)在form视图的底部提供了消息板跟关注者功能。我们经常需要添加这些消息传递逻辑到我们的模块,下面就开始操作

  • 添加模型依赖到__manifest__.pydepned

'depends':['todo_app','mail']
  • 继承mail.thread模型,mail.thread模型是一个抽象模型,它没有实际的数据库表结构,是用来作为混合类来添加想要的功能来使用的
_name = 'todo.task'
_inherit = ['todo.task','mail.thread']
  • 添加关注者窗口化部件到form视图中,把下面的xml代码插入到我们前面编写的form继承视图view_form_todo_task_inherited的’arch’元素下
                <sheet position="after">
                    <div class="oe_chatter">
                        <field name="message_follower_ids"
                               widget="mail_followers"/>
                        <field name="message_ids" widget="mail_thread"/>
                    </div>
                </sheet>

修改数据记录

  • 不同于views中的xml,普通的数据记录没有XML arch结构,无法被Xpath定位到,但我们任然可以通过来插入或者修改普通数据记录。(x不存在就是插入,存在即为修改)

修改菜单,动作记录。

        <!--modify menu item-->
    <record id="todo_app.menu_todo_task" model="ir.ui.menu">
        <field name="name">My To-Do</field>
    </record>

        <record model="ir.actions.act_window"
                id="todo_app.action_todo_task">
            <field name="context">
                {'search_default_filter_my_tasks':True}
            </field>
        </record>
  • 我们既然使用了新的继承类,顺便使用上面的代码来修改以前在todo_app模块的菜单的名称。 动作视图中有一个可选参数为context,这个参数可以为视图中的字段跟过滤器提供默认值。在这里,我们使用它来为设定默认的过滤条件为以前设置过的My Tasks过滤器。注意这里以search_default_作为前缀.

修改安全记录规则

  • 前一个章节中,我们的todo_app的记录规则在于创立task的用户才能看到对应的task,现在由于社交功能的加入,只要是task的关注者,都能看到该task. 我们创建/todo_user/security/todo_access_rules.xml文件,添加以下代码。
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
    <data noupdate="1">
        <record id="todo_app.todo_task_per_user_rule"
                model="ir.rule">
            <field name="name">ToDo Tasks for owner and followers</field>
            <field name="model_id" ref="model_todo_task"/>
            <field name="groups" eval="[(4, ref('base.group_user'))]"/>
            <field name="domain_force">
                ['|',('user_id','in',[user.id,False]),
                    ('message_follower_ids','in',[user.partner_id.id])]
            </field>
        </record>
    </data>
</odoo>
  • 规则记录运行在当前用户的上下文中,因为task的关注者是partners,所以需要使用user.partner_id来代替user.id
  • groups字段是一个一对多的关系, 4在这里代表把base.group_user这个模型中的所有记录添加到一个list里
  • 我们重新添加了domain规则。 task只在以下情况显示:1.task的负责人不存在或者是当前用户 2.当前用户是关注者其中一个(‘message_follower_ids’,‘in’,[user.partner_id.id])