Odoo 8.0深入浅出开发教程(七) Odoo开发之扩展现有模块

时间:2021-04-22 16:22:36

8.扩展现有模块-继承机制

即使是对于现有的模块,推荐的做法也是通过新建一个模块来达到扩展和修改现有模块的目的。具体方法就是在python中的类里面使用 _inherit 属性。这标识了将要扩展的模块。新的模型继承了父模型的所有特性,我们只需要声明一些我们想要的修改就行了。通过这种继承机制的修改可从模型到视图到业务逻辑等对原模块进行全方位的修改。

实际上,Odoo模型在我们定义的模型之外,它们都在注册中心注册了的,所谓全局环境的一部分,可以用 self.env[model name] 来引用之。比如要引用 res.partner 模型,我们就可以写作self.env['res.partner'] 。

8.1 给模块增加field

如下代码就是首先通过 _inherit 继承原模块,然后再增加一些field:

from openerp import models, fields, apiclass TodoTask(models.Model):_inherit = 'todo.task'user_id = fields.Many2one('res.users',string='Responsible')date_deadline = fields.Date('Deadline')

关于 res.users 和 res.partner 具体是雇员还是合作伙伴什么的,这个以后再摸清楚,这里先简单将其看作一个SQL表格,然后Many2one前面讲过了就是根据某个给定的SQL表格来生成一个下拉选单,具体是引用的该SQL表格的那个表头属性,这里应该还有一个细节讨论。

不管怎么说,现在我们通过新建一个模块 todo_user ,如前面描述的将模块设置配置好之后,原模块 todo_app 的todo.task模型就增加了新的两个field了,也就是两个新的表头了。

8.2 修改已有的field

按照上面的继承机制,我们可以如上类似处理,只修改你希望更改的某个field的某个属性即可。如下:

name = fields.Char(help="can I help you")

这样原模型的namefield额外增加了help帮助信息了。

Odoo 8.0深入浅出开发教程(七) Odoo开发之扩展现有模块

Figure 26: help帮助信息

8.3 重载原模型的方法

读者一定已经想到了,类似的在这种继承机制下,可以通过重写原模型的方法来重载该方法。事实上确实可以这样做,而这里要讲的是还有一种更加优雅的继承原模型的方法,那就是通过 super()来调用父类的方法3

首先我们看到下面这个例子:

@api.multi
def do_clear_done(self):
domain = [('is_done', '=', True),
'|', ('user_id', '=', 'self.env.uid'),
('user_id','=',False)]
done_recs = self.search(domain)
done_recs.write({'active':False})
return True

这里涉及到Odoo新API的一些东西,这里先浅尝辄止讲一下。

8.3.1 什么是Recordset

Odoo8引入了一种新的ORM API,老的API也兼容,但推荐使用新的API,因为新的API更加简洁和方便。

首先是模型(model),其对应的就是python的类,具体类的实例就是对应现实世界的某个对象。然后老式的简单ORM封装就是将这些类的具体某些数据对应到SQL的数据库的一条记录(record)中去。新的API引入一个核心的概念就是 Recordset ,Recordset是个什么东西呢?就是前面讲的某一个模型(类)的所有对象(具体的实例)的集合就是一个Recordset对象。——这是recordset最大的情况,一个重要的限定条件就是其内元素必定是相同模型的,由这个最大的集合情况然后删除过滤掉一些元素(记录)之后仍然是recordset对象。

按照官方文档的描述是,一个Recordset对象应该是已经排序了的同一模型的对象的集合。他还指出虽然现在还可以存放重复的元素,这个以后可能会变的。同时你从名字可能猜到这个Recordset对象应该支持集合的一些操作,事实确实如此。

比如Recordset支持如下运算:

record in recset1     # include
record not in recset1 # not include
recset1 + recset2 # extend
recset1 | recset2 # union
recset1 & recset2 # intersect
recset1 - recset2 # difference
recset.copy() # copy the recordset (not a deep copy)

上面的操作只有 + 还保留了次序,不过recordset是可以排序的,关于次序比如使用:

for record in recordset:
print(record)

具体的次序是否像集合set一样是不一定的还是如何呢?这里需要进一步的讨论。

8.3.2 Odoo里面的domain语法

本小节主要参考了 这个网页 。

Odoo里面的domain语法使用比较广泛,其就好像一个过滤器,应该对应的是SQL的SELECT语句。最基本的语句形式是 [('field_name', 'operator', value)]

field_name
必须是目标模型的有效field名字。
operator
比如是一个字符串,可用的值有:  = != > >= < <= like ilike , 此外还有"in", "not in", "parent_left", "child_of", "parent_right"。这里的parent和chind似乎是某种记录的关系,先暂时略过。其他的意义都是很明显的。
value
必须是和前面的field_name类型相同的某个值。

然后这些圆括号包围的基本语句可以用以下几个逻辑运算符连接: & | ! ,其中 & 是默认的逻辑运算连接符,也就是你看到两个圆括号表达式中间没有逻辑运算连接符,则要视作其间加入了 & 。具体形式大概类似这样:

[('field_name1', 'operator', value), '!',  
('field_name2', 'operator', value), '|',
('field_name3', 'operator', value),('field_name4', 'operator', value)]

多个逻辑运算符的情况有点复杂,具体是 ! 先解析,其只作用于后面的第一个元素;然后 & 和 |作用于后面的两个元素。一个简单的解析步骤是先将 ! 解析进去,比如是解析为不是等,然后再将 |解析进去,相当于一个并联电路接进来,然后所有的过滤条件组成一个大的串联过滤线路。这样上面的表达式就解析为:

1表达式 and 2表达式否 and 3表达式或4表达式

然后前面的那个domain:

domain = [('is_done', '=', True),
'|', ('user_id', '=', 'self.env.uid'),
('user_id','=',False)]

应该解析为:

is_done是True and user_id 是self.env.uid 或 user_id是False

8.3.3 recordset的search方法

一个recordset对象调用其search方法还是返回一个recordset对象。

search方法接受一个参数,这个参数就是前面谈论的基于Odoo domain语法的过滤器表达式。

所以下面这个表达式:

self.env[’res.users’].search([(’login’, ’=’, ’admin’)])

的含义就是调用 res.users 这个表格或者说recordset,然后执行search方法,具体选中的record是login这个字段等于admin的。

好了前面那个 do_clear_done 函数我们应该完全理解了,首先 @api.multi 告诉我们这个函数里面的self是一个recordset,然后domain的语法是: is_done是True或说被勾选了,然后要某该记录的user_id等于当前用户的id self.env.uid ,要某 user_id 值为False(不清楚什么情况)。

接下来执行search方法,返回的done_recs也是一个recordset对象,对于这些recordset对象执行了 write 方法,其接受一个字典值,就是直接更改SQL表格里面的某个表头(属性),将其改为某个值。值得一提的是,recordset调用write方法会将本recordset内所有的record都进行修改操作的。

前面讲到通过 super() 来继承修改原模型的某个方法,请看下面的例子:

@api.one
def do_toggle_done(self):
if self.user_id != self.env.user:
raise Exception('Only the responsible can do this!')
else:
return super(TodoTask, self).do_toggle_done()

这里 @api.one 自动遍历目标recordset,然后方法里面的self就是一个record。这里程序的逻辑很简单,就是如果用户名不是当前登录用户(因为todo task管理只是自己管理自己的任务计划),那么将会报错。如果是那么就调用之前的方法。

8.4 视图xml文件的继承式修改

一个初步的继承式修改视图xml文件如下所示:

<?xml version="1.0"?><openerp><data><record id="view_form_todo_task_inherited" model="ir.ui.view"><field name="name">Todo Task form – User extension</field><field name="model">todo.task</field><field name="inherit_id" ref="todo_app.view_form_todo_task"/><field name="arch" type="xml"><field name="name" position="after"><field name="user_id" /></field><field name="is_done" position="before"><field name="date_deadline" /></field><field name="name" position="attributes"><attribute name="string">I have to...</attribute></field></field></record></data></openerp>

我们可以看到其通过这样的语句:

<field name="inherit_id" ref="todo_app.view_form_todo_task"/>

对xml视图进行了继承。这里是要对from视图进行修改,就继承的原form视图的id。

8.4.1 视图元素添加

首先我们来看视图元素的添加问题。Odoo提供了这样的定位语法:

<field name="name" position="after">
<field name="user_id" />
</field>

其具体对应的是所谓的XPath语法,比如 <field name="is_done"> 对应的是:

//field[@name]='is_done'

除了field,其他的tag如sheet、group等等都是可以用的,属性name最常使用,其他的属性也是可以用的。定位到具体的标签之后,需要使用 position 来指明插入点。

inside
默认的就是inside,也就是插入定位标签之内。
before
插入定位标签之前。
after
插入定位标签之后。
replace
替换掉定位标签的元素,如果使用空内容,则就是删除原标签元素。

比如这个例子

<field name="name" position="after">
<field name="user_id" />
</field>
<field name="is_done" position="before">
<field name="date_deadline" />
</field>

的意思就是找到field name="name"的那个标签,然后在它的后面插入 <field name="user_id" />

然后找到field name="is_done"的那个标签,在它的前面插入 <field name="date_deadline" /> 。

8.4.2 原视图元素属性修改

position如果设置为 attributes ,则可以具体对原标签元素的某个属性进行修改。

attributes
修改定位标签元素的某个属性。

比如这样:

<field name="name" position="attributes">
<attribute name="string">I am going to</attribute>
</field>

再如:

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

之前的active field没必要显示出来了,可以将这个字段的 invisible 属性设置为1,让这个字段在视图上不显示即可。前面讲到replace说到可以删除某个标签元素,但一般不建议这样做,因为可能其他扩展模块又依赖这个标签元素。最好就是将它的 invisible 属性修改一下即可。

读者可以看到 之前那个form视图 。

经过如上的修改,现在成了这个样子了:

Odoo 8.0深入浅出开发教程(七) Odoo开发之扩展现有模块

8.5 多态继承

_inherit 继承也可以继承多个模型,如下所示写成一个列表值即可,然后 _name 比如指明了,因为有多个继承模型,不指明Odoo是不清楚要继承谁的 _name 的。

_name = 'todo.task'
_inherit = ['todo.task', 'mail.thread']

mail.thread是一个抽象模型,抽象模型没有数据库表达,没有实际创立SQL表格。抽象模型最适合被混合继承使用。要创建一个抽象模型就是继承自 models.AbstractModel 而不是 models.Model 。

8.6 修改其他数据文件

不像视图文件的 arch 结构下的xml可以用XPath表达式,其他xml数据文件则要采取不同的方法来修改之。

8.6.1 删除记录

这是删除记录的语法

<delete model="ir.rule" search="[('id', '=',
ref('todo_app.todo_task_user_rule'))]" />

使用的是delete标签,然后模型对应某个recordset,然后使用search方法,这里的ref语句还不太清楚。

8.6.2 更新数据

其他记录若不像删除,则使用 <record id="x" model="y"> 这样的语法,若该记录不存在,则会创建,若存在则会修改其中的某些值。

下面这个例子是用来修改record rule权限文件的,将其改成本人和follower都可以看你的todo task。

<?xml version="1.0" encoding="utf-8"?><openerp><data noupdate="0"><delete model="ir.rule" search="[('id', '=',     ref('todo_app.todo_task_user_rule'))]" /><record id="todo_task_per_user_rule" model="ir.rule"><field name="name">ToDo Tasks only for owner</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></openerp>

这里的一些细节我们可以先暂时略过,记住这种记录数据删除和更新的方法就是了。然后看到data标签的 noupdate 属性,如果设置为"0"的话就更新数据,这通常是在开发期这样设置,如果在运行期则设置为"1",也就是接下来模块升级也不会更新本data数据,通常为了运行期稳定会这样设置。

8.7 委托继承

除了前面谈论的 _inherit 继承外,Odoo还提供了一种继承机制,叫做什么委托继承(delegation inheritance)。委托继承特别适合继承官方内置的现有模型。按照官方文档的说法,委托继承一些值是存放于不同的SQL表格中的,所以其似乎是通过一种SQL连接机制来达到继承效果的。然后委托继承只有fields被继承了,而方法没有被继承(因为那些方法又不是存放在SQL表格里面的。)。

具体委托继承的详情分析还需要进一步讨论。

9.理解模型内的数据文件

9.1 理解外部id

所有的记录在Odoo数据库中都有一个独一无二的标识码id,Odoo是通过 ir.model.data 模型来管理这些外部id的。ir.model.data模型对应的SQL表格是 ir_model_data 。这个表格里面存储着各个模型外部名字ID(通过record标签的id属性指定)和具体数据库某个表格ID的映射关系。这个表格有四个字段值得引起我们的注意:

我们执行:

SELECT 
id, name, module, model, res_id
FROM
public.ir_model_data
WHERE
MODULE = 'qingjia'
;

注意WHERE字句后面的字段要大写。则有:

  id  |                name                | module  |         model         | res_id 
------+------------------------------------+---------+-----------------------+--------
3707 | model_qingjia_qingjd | qingjia | ir.model | 153
3708 | field_qingjia_qingjd_startdate | qingjia | ir.model.fields | 1703
3709 | field_qingjia_qingjd_create_date | qingjia | ir.model.fields | 1704
3710 | field_qingjia_qingjd_name | qingjia | ir.model.fields | 1705
3711 | field_qingjia_qingjd_create_uid | qingjia | ir.model.fields | 1706
3712 | field_qingjia_qingjd_state | qingjia | ir.model.fields | 1707
3713 | field_qingjia_qingjd_days | qingjia | ir.model.fields | 1708
3714 | field_qingjia_qingjd_reason | qingjia | ir.model.fields | 1709
3715 | field_qingjia_qingjd_write_date | qingjia | ir.model.fields | 1710
3716 | field_qingjia_qingjd_write_uid | qingjia | ir.model.fields | 1711
3717 | field_qingjia_qingjd_id | qingjia | ir.model.fields | 1712
3718 | access_qingjia_qingjd | qingjia | ir.model.access | 189
3719 | action_qingjia_qingjd | qingjia | ir.actions.act_window | 130
3720 | qingjia_qingjd_form | qingjia | ir.ui.view | 298
3721 | qingjia_qingjd_tree | qingjia | ir.ui.view | 299
3722 | menu_qingjia | qingjia | ir.ui.menu | 133
3723 | menu_qingjia_qingjiadan | qingjia | ir.ui.menu | 134
3724 | menu_qingjia_qingjiadan_qingjiadan | qingjia | ir.ui.menu | 135
3725 | wkf_qingjia | qingjia | workflow | 1
3726 | act_draft | qingjia | workflow.activity | 1
3727 | act_confirm | qingjia | workflow.activity | 2
3728 | act_accept | qingjia | workflow.activity | 3
3729 | act_reject | qingjia | workflow.activity | 4
3731 | qingjia_draft2confirm | qingjia | workflow.transition | 1
3732 | qingjia_confirm2accept | qingjia | workflow.transition | 2
3733 | qingjia_confirm2reject | qingjia | workflow.transition | 3

然后我们看到

Odoo 8.0深入浅出开发教程(七) Odoo开发之扩展现有模块

Figure 28: 记录的外部id

这里的完整ID就对应具体的那条记录,其是由module和name这两个字段的值组合而成的,比如说 qingjia.menu_qingjia ,具体格式就是 <module>.name 。然后具体的内部引用对应的是 ir_ui_menu这个SQL表格(根据上面的model ir.ui.menu 而来)中的133号记录(根据 res_id )而来。

9.2 使用外部id

在Odoo新的API下,你可以通过这样 self.env.ref('external id') 的简介语法来通过外部id来引用具体的某个record。

9.3 导出或导入数据文件

在tree列表视图下,有具体的导入或到处数据文件功能。导入需要csv格式,导出可以是csv格式或excel格式。

值得一提的是 security 文件夹下的 ir.model.access.csv 文件名字是固定的,然后其他一些访问权限规则最好是单独用文件编写。

然后视图的xml文件讲起来也是官方内置模块的对象数据文件,不过这里是不能在网页下点击操作的,必须手工编写xml文件来完成。

workflow的xml文件推荐放在workflow文件夹下。

在一定要手工编写XML文件的情况下,前面已经有所讨论了,这里进一步进行一些补充说明。

9.4 快捷输入标签

一般的记录声明就是使用的record标签,然后加上id属性和model属性。如下所示:

<record id="group_purchase_user" model="res.groups">
<field name="name">User</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
<field name="category_id" ref="base.module_category_purchase_management"/>
</record>

同时前面提到了 menuitem 这样的快捷输入标签可以这样使用。

<menuitem id="menu_qingjia" name="请假" sequence="0"></menuitem>

这些快捷标签的使用很方便的,下面是一些可用的快捷输入标签清单:

<act_window>
对应模型  ir.actions.act_window ,视窗动作对象。
<menuitem>
对应模型  ir.ui.menu ,菜单对象。
<report>
对应模型  ir.actions.report.xml 打印动作对象。
<template>
对应模型  ir.ui.view ,视图的模板文件对象。
<url>
对应模型  ir.actions.act_url URL打开动作对象。

其他的就好用record标签的标准形式来引入对象数据记录了。

9.5 用field标签设置值

具体指定某个字段的值如上使用field标签,然后用name指明某个字段。

具体的值字符串不需要加上双引号"",直接写上即可。布尔值直接写上0,1或者False,True都是可以的。返回日期或日期时间采用如下格式也可以正确被转换: YYYY-MM-DD 和 YYYY-MM-DD HH:MI:SS 。

9.5.1 eval语法

前面也有所涉及,field的值支持用eval语法来运算某个表达式获得。如下所示,其内任意的python表达式都是可以的:

<field name="expiration_date"
eval="(datetime.now() + timedelta(-1)).strftime('%Y-%m-%d')" />

datatime模块下的datetime还有timedelta类已经被引入进来可以直接使用了。datetime模块的介绍不是这里的重点,所以这里的细节略过了。

然后ref函数也可以直接使用:

<field name="user_id" eval="ref('base.group_user')" />

上面使用ref函数引用记录外部id base.group_user ,其是res_groups表格的第五条记录,具体在群组里面是employee(人力资源/雇员)组。

<value model="sale.order"
eval="obj(ref('test_order_1')).amount_total" />

这里的obj是根据某个记录来得知具体的某个模型(还不清楚??)

9.5.2 ref属性

ref函数在这里对应的field主要是Many2One类型的field。不过更简单的可以不用eval而直接用ref属性来调用。比如上面的就可以简单写为:

<field name="user_id" ref="base.group_user" />

9.5.3 One2many和Many2many的eval赋值

<field name="tag_ids"
eval="[(6,0,
[ref('vehicle_tag_leasing'),
ref('fleet.vehicle_tag_compact'),
ref('fleet.vehicle_tag_senior')]
)]" />
  • (0,_ ,{'field': value}) 这将创建一个新的记录并连接它
  • (1,id,{'field': value}): 这是更新一个已经连接了的记录的值
  • (2,id,_) 这是删除或取消连接某个已经连接了的记录
  • (3,id,_) 这是取消连接但不删除一个已经连接了的记录
  • (4,id,_) 连接一个已经存在的记录
  • (5,_,_) 取消连接但不删除所有已经连接了的记录
  • (6,_,[ids]) 用给出的列表替换掉已经连接了的记录

这里的下划线一般是0或False。