即使是对于现有的模块,推荐的做法也是通过新建一个模块来达到扩展和修改现有模块的目的。具体方法就是在python中的类里面使用 _inherit
属性。这标识了将要扩展的模块。新的模型继承了父模型的所有特性,我们只需要声明一些我们想要的修改就行了。通过这种继承机制的修改可从模型到视图到业务逻辑等对原模块进行全方位的修改。
实际上,Odoo模型在我们定义的模型之外,它们都在注册中心注册了的,所谓全局环境的一部分,可以用 self.env[model name]
来引用之。比如要引用 res.partner
模型,我们就可以写作self.env['res.partner']
。
如下代码就是首先通过 _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了,也就是两个新的表头了。
按照上面的继承机制,我们可以如上类似处理,只修改你希望更改的某个field的某个属性即可。如下:
name = fields.Char(help="can I help you")
这样原模型的namefield额外增加了help帮助信息了。
Figure 26: help帮助信息
读者一定已经想到了,类似的在这种继承机制下,可以通过重写原模型的方法来重载该方法。事实上确实可以这样做,而这里要讲的是还有一种更加优雅的继承原模型的方法,那就是通过 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的一些东西,这里先浅尝辄止讲一下。
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一样是不一定的还是如何呢?这里需要进一步的讨论。
本小节主要参考了 这个网页 。
Odoo里面的domain语法使用比较广泛,其就好像一个过滤器,应该对应的是SQL的SELECT语句。最基本的语句形式是 [('field_name', 'operator', value)]
= != > >= < <= like ilike
, 此外还有"in", "not in", "parent_left", "child_of", "parent_right"。这里的parent和chind似乎是某种记录的关系,先暂时略过。其他的意义都是很明显的。
然后这些圆括号包围的基本语句可以用以下几个逻辑运算符连接: &
|
!
,其中 &
是默认的逻辑运算连接符,也就是你看到两个圆括号表达式中间没有逻辑运算连接符,则要视作其间加入了 &
。具体形式大概类似这样:
[('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
一个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管理只是自己管理自己的任务计划),那么将会报错。如果是那么就调用之前的方法。
一个初步的继承式修改视图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。
首先我们来看视图元素的添加问题。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 来指明插入点。
比如这个例子
<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" />
。
position如果设置为 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视图 。
经过如上的修改,现在成了这个样子了:
_inherit
继承也可以继承多个模型,如下所示写成一个列表值即可,然后 _name
比如指明了,因为有多个继承模型,不指明Odoo是不清楚要继承谁的 _name
的。
_name = 'todo.task' _inherit = ['todo.task', 'mail.thread']
mail.thread是一个抽象模型,抽象模型没有数据库表达,没有实际创立SQL表格。抽象模型最适合被混合继承使用。要创建一个抽象模型就是继承自 models.AbstractModel
而不是 models.Model
。
不像视图文件的 arch 结构下的xml可以用XPath表达式,其他xml数据文件则要采取不同的方法来修改之。
这是删除记录的语法
<delete model="ir.rule" search="[('id', '=', ref('todo_app.todo_task_user_rule'))]" />
使用的是delete标签,然后模型对应某个recordset,然后使用search方法,这里的ref语句还不太清楚。
其他记录若不像删除,则使用 <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数据,通常为了运行期稳定会这样设置。
除了前面谈论的 _inherit
继承外,Odoo还提供了一种继承机制,叫做什么委托继承(delegation inheritance)。委托继承特别适合继承官方内置的现有模型。按照官方文档的说法,委托继承一些值是存放于不同的SQL表格中的,所以其似乎是通过一种SQL连接机制来达到继承效果的。然后委托继承只有fields被继承了,而方法没有被继承(因为那些方法又不是存放在SQL表格里面的。)。
具体委托继承的详情分析还需要进一步讨论。
所有的记录在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
然后我们看到
Figure 28: 记录的外部id
这里的完整ID就对应具体的那条记录,其是由module和name这两个字段的值组合而成的,比如说 qingjia.menu_qingjia
,具体格式就是 <module>.name
。然后具体的内部引用对应的是 ir_ui_menu
这个SQL表格(根据上面的model ir.ui.menu
而来)中的133号记录(根据 res_id
)而来。
在Odoo新的API下,你可以通过这样 self.env.ref('external id')
的简介语法来通过外部id来引用具体的某个record。
在tree列表视图下,有具体的导入或到处数据文件功能。导入需要csv格式,导出可以是csv格式或excel格式。
值得一提的是 security 文件夹下的 ir.model.access.csv
文件名字是固定的,然后其他一些访问权限规则最好是单独用文件编写。
然后视图的xml文件讲起来也是官方内置模块的对象数据文件,不过这里是不能在网页下点击操作的,必须手工编写xml文件来完成。
workflow的xml文件推荐放在workflow文件夹下。
在一定要手工编写XML文件的情况下,前面已经有所讨论了,这里进一步进行一些补充说明。
一般的记录声明就是使用的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>
这些快捷标签的使用很方便的,下面是一些可用的快捷输入标签清单:
ir.actions.act_window
,视窗动作对象。
ir.ui.menu
,菜单对象。
ir.actions.report.xml
打印动作对象。
ir.ui.view
,视图的模板文件对象。
ir.actions.act_url
URL打开动作对象。
其他的就好用record标签的标准形式来引入对象数据记录了。
具体指定某个字段的值如上使用field标签,然后用name指明某个字段。
具体的值字符串不需要加上双引号"",直接写上即可。布尔值直接写上0,1或者False,True都是可以的。返回日期或日期时间采用如下格式也可以正确被转换: YYYY-MM-DD
和 YYYY-MM-DD HH:MI:SS
。
前面也有所涉及,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是根据某个记录来得知具体的某个模型(还不清楚??)
ref函数在这里对应的field主要是Many2One类型的field。不过更简单的可以不用eval而直接用ref属性来调用。比如上面的就可以简单写为:
<field name="user_id" ref="base.group_user" />
<field name="tag_ids" eval="[(6,0, [ref('vehicle_tag_leasing'), ref('fleet.vehicle_tag_compact'), ref('fleet.vehicle_tag_senior')] )]" />
这里的下划线一般是0或False。