欢迎阅读Odoo 8.0新API指南¶
概览
记录/记录集(Record/Recordset) 与 模型(Model)¶
OpenERP/Odoo 8.0版引入了一个新的ORM API。
它的目标是提供更连贯和简洁的语法形式,并保持前后的兼容性。
新的API保留了之前的基础设计,例如 模型(Model)和 记录(Record),但是增加了新的概念,例如 环境(Environment)和 记录集(Recordset)。
之前API的一些内容在这个版本里也没有变化,例如 域(domain)的语法。
模型(Model)¶
一个模型代表了一个业务对象。
它本质上是一个类,包含各种类有关的功能定义和存储在数据库中的字段。 所有定义在模型中的方法都可以被模型本身直接调用。
现在编程范式有所改变,你不应该直接访问模型,而是应该使用 记录集(RecordSet) 参见 记录集(Recordset)
要实例化一个模型,你必须继承 openerp.model.Model:
from openerp import models, fields, api, _
class MyModel(models.Model):
_name = 'a.model' # 模型名称会被用作数据库表名
firstname = fields.Char(string="Firstname")
记录集(Recordset)¶
所有模型的实例同时也是对应记录集的实例。 一个记录集表达了对应模型的经过排序的记录的集合。
你可以在记录集里调用方法:n:
class AModel(Model):
# ...
def a_fun(self):
self.do_something() # 这里self是一个记录集,介于 类(class)与 集合(set)之间的混合体
record_set = self
record_set.do_something()
def do_something(self):
for record in self:
print record
在这个例子里,方法定义在模型一级,但是当运行这段代码时, self
变量实际上是包含很多记录的一个记录集的实例。
所以传入 do_something
的 self 是一个包含一系列记录的记录集。
如果你用 @api.one
装饰一个方法的话,它会自动遍历当前记录集的记录,然后这时的 self 就是当前这条记录。
如我们在 记录(Record) 中所述,你现在访问的就是一个伪 Active-Record模式。
注解
如果记录集只含有一条记录,你把这个装饰器用在该记录集上会导致中断!! (If you use it on a RecordSet it will break if recordset does not contains only one item.!!)
支持的操作¶
记录集也支持集合操作,你可以使用 联合、交、补 等运算:n:
record in recset1 # 属于
record not in recset1 # 不属于
recset1 + recset2 # 扩展(extend)
recset1 | recset2 # 联合(union)
recset1 & recset2 # 交集(intersect)
recset1 - recset2 # 差补/相对补集(difference)
recset.copy() # 记录集的浅复制(被复制对象的所有变量都含有与原来的对象相同的值,而其所有的对其他对象的引用都仍然指向原来的对象)
只有 +
操作符保留集合元素的顺序
记录集也可以被排序:n:
sorted(recordset, key=lambda x: x.column)example
有用的辅助方法¶
新的API为记录集提供了很多有用的辅助方法。
你可以轻易的过滤已有的记录集:n:
recset.filtered(lambda record: record.company_id == user.company_id)
# 或使用字符串
recset.filtered("product_id.can_be_sold")
你可以对一个记录集排序:n:
# 按名称对记录排序
recset.sorted(key=lambda r: r.name)
你也可以使用 operator 模块
from operator import attrgetter
recset.sorted(key=attrgetter('partner_id', 'name'))
有一个映射记录集的辅助方法:n:
recset.mapped(lambda record: record.price_unit - record.cost_price)
# 返回名称列表
recset.mapped('name')
# 返回合作者记录集
recset.mapped('invoice_id.partner_id')
ids 属性¶
ids 属性是记录集的一个特殊属性,当记录集包含一个或更多记录时它会返回对应ids。
记录(Record)¶
一个记录反映了从数据库中取得的“模型记录实例”。它使用缓存和查询生成了数据库记录条目的抽象:
>>> record = self
>>> record.name
toto
>>> record.partner_id.name
partner name
记录的显示名称(display name)¶
在新API里一个叫显示名称的概念被引入。它使用 name_get
底层方法。
所以如果你希望覆盖显示名称,你需要覆盖 display_name
字段。
示例
如果你希望覆盖显示名称和计算出的相关名称,你需要覆盖 name_get
。
示例
Active Record 模式¶
在新API引入的新特性之一是对active record模式的基础支撑。你现在可以使用设置属性(setting properties)来写入数据库:n:
record = self
record.name = 'new name'
上面的例子会更新缓存中的值并调用写方法来触发向数据库的写入动作。
Active Record模式 注意事项¶
使用Active Record模式写值必须要小心,因为每一个值的指定都会触发数据库的写操作:n:
@api.one
def dangerous_write(self):
self.x = 1
self.y = 2
self.z = 4
在这个例子里每一个赋值都会触发写操作。
因为这个方法使用了 @api.one
装饰,对记录集里的每个记录的写操作都会被调用3次,那么如果你的记录集有10条记录,一共会有 10*3 = 30 次写操作。
这在高负载任务里会导致性能问题。你应该这样写:n:
def better_write(self):
for rec in self:
rec.write({'x': 1, 'y': 2, 'z': 4})
# 或者
def better_write2(self):
# 给所有记录赋相同值
self.write({'x': 1, 'y': 2, 'z': 4})
环境(Environment)¶
新API引入了环境的定义。它的主要目的是提供对于 记录指针(cursor)、用户id(user_id)、模型(model)、上下文(context)、记录集(Recordset)和缓存(cache)的封装。

有了这个附加功能,你就不用传递那么多的方法参数了:n:
# 以前
def afun(self, cr, uid, ids, context=None):
pass
# 现在
def afun(self):
pass
为了访问到环境,你需要:n:
def afun(self):
self.env
# 或者
model.env
环境应该是不可变的,不能在方法里进行修改,因为它还存储着记录集的缓存等等信息。
修改环境¶
如果你需要修改当前的上下文,你需要使用 with_context() 方法:n:
self.env['res.partner'].with_context(tz=x).create(vals)
要小心不要使用如下方法修改当前记录集:n:
self = self.env['res.partner'].with_context(tz=x).browse(self.ids)
它会在重新查询后修改当前记录集里的记录,从而导致缓存和记录集之间的不连贯。
改变用户¶
环境提供了一个切换用户的辅助方法:n:
self.sudo(user.id)
self.sudo() # 缺省会使用 SUPERUSER_ID
# 或者
self.env['res.partner'].sudo().create(vals)
访问当前用户¶
self.env.user
使用XML id 获取记录¶
self.env.ref('base.main_company')
清理环境缓存¶
在前面我们介绍了环境维护着多种缓存,这些缓存用于模型、字段等类。
有时候你可能必须要使用记录指针(cursor)来直接插入/写数据,这种情况下你需要使这些缓存无效:n:
self.env.invalidate_all()
一般动作(Common Actions)¶
搜索¶
搜索并没有太大变化。可惜的是宣称的域(domain)的变动没有在8.0版本里实现。
下面是一些主要的变化。
search¶
现在 seach
方法直接返回一个记录集:n:
>>> self.search([('is_company', '=', True)])
res.partner(7, 6, 18, 12, 14, 17, 19, 8,...)
>>> self.search([('is_company', '=', True)])[0].name
'Camptocamp'
你可以使用env来调用 search :n:
>>> self.env['res.users'].search([('login', '=', 'admin')])
res.users(1,)
search_read¶
search_read
方法加入进来了。它会执行一个 search 并返回一个字典(dict)列表(list)。
这里我们获取所有合作伙伴名称:n:
>>> self.search_read([], ['name'])
[{'id': 3, 'name': u'Administrator'},
{'id': 7, 'name': u'Agrolait'},
{'id': 43, 'name': u'Michel Fletcher'},
...]
search_count¶
search_count
方法返回符合搜索域(domain)定义的记录数量:n:
>>> self.search_count([('is_company', '=', True)])
26L
检索¶
检索是从数据获取记录的标准方法。现在检索会返回一个记录集:n:
>>> self.browse([1, 2, 3])
res.partner(1, 2, 3)
更多关于记录的信息请参考 记录(Record)
写入¶
使用 Active Record 模式¶
现在可以用 Active Record 模式来写入:n:
@api.one
def any_write(self):
self.x = 1
self.name = 'a'
更多关于Active Record 模式来写入的小窍门,请参考 记录(Record)
传统的写入方式仍然可用。
从记录集写入¶
从记录集写入:n:
@api.multi
...
self.write({'key': value })
# 它将写入到所有记录里
self.line_ids.write({'key': value })
它将写入到所有关联的线索(line)的记录里。
多对多(Many2many) 一对多(One2many) 写入行为¶
一对多(One2many) 和 多对多(Many2many)字段有一些特殊行为需要考虑到。 At that time (this may change at release) using create on a multiple relation fields will not introspect to look for the relation.
self.line_ids.create({'name': 'Tho'}) # 这个调用将会失败,因为没有指定订单(order)
self.line_ids.create({'name': 'Tho', 'order_id': self.id}) # 这个调用将会正常执行
self.line_ids.write({'name': 'Tho'}) # 这个调用将会写到所有相关的线索(line)记录里
当在一个装饰了 @api.onchange 的方法里添加新的关联记录时,你可以使用 openerp.models.BaseModel.new()
构造方法。这个方法会创建一个未提交至数据库的记录,包含一个 openerp.models.NewId
类型的id。
self.child_ids += self.new({'key': value})
这种记录在表单保存时会写入数据库。
拷贝¶
注解
标题得改,目前还是有很多问题!!!
演习(Dry run)¶
你可以通过 do_in_draft
这个环境上下文管理器的辅助方法来只在缓存中执行动作。
使用记录指针¶
记录、记录集和环境共用同一个记录指针。
所以你可以用如下方法来访问记录指针:n:
def my_fun(self):
cursor = self._cr
# 或者
self.env.cr
然后你就可以像以前的API里一样使用记录指针了。
使用线程¶
使用线程时你必须创建自己的记录指针,并且在每个线程里初始化一个新的环境。 数据库操作提交在提交记录指针时完成:n:
with Environment.manage(): # 类方法
env = Environment(cr, uid, context)
新 ids¶
当创建一个包含计算字段的记录或模型时,记录集的记录只在内存里。此时记录的 id 将是一个 openerp.models.NewId
类型的虚拟id。
所以如果你在你的代码里(例如一段SQL查询)用到了记录 id 的话,你应该先检查它是否存在:
if isinstance(current_record.id, models.NewId):
# 你的代码
字段¶
现在字段是类的属性:
from openerp import models, fields
class AModel(models.Model):
_name = 'a_name'
name = fields.Char(
string="Name", # 字段的可选标签
compute="_compute_name_custom", # 在计算字段里转换的字段
store=True, # 如果计算,就保存结果
select=True, # 强制对字段索引
readonly=True, # 在视图中字段将是只读的
inverse="_write_name" # 在修改时触发
required=True, # 必需的字段
translate=True, # 启用翻译
help='blabla', # 工具提示帮助文本
company_dependent=True, # 把列转换到 ir.property
search='_search_function' # 自定义搜索功能,主要和计算一起使用
)
# string 关键字不是必需的
# 缺省情况下使用大写属性名称
name = fields.Char() # 合法定义
字段类型¶
浮点型(Float)¶
存储浮点值。不支持 NULL 值,如果未设定值则返回 0.0。如果设定了 digits 选项,那么将使用数值(numeric)类型:
afloat = fields.Float()
afloat = fields.Float(digits=(32, 32))
afloat = fields.Float(digits=lambda cr: (32, 32))
特殊选项:
- digits: 强制使用数据库的数值(numeric)类型。参数可以是一个元组(tuple) (int len, float len) 或者一个使用记录指针(cursor)作为参数的、返回值为一个元组(tuple)的可调用方法
日期型(Date)¶
存储日期。 这种字段提供一些辅助方法:
context_today
返回基于时区(tz)的当日日期字符串today
返回当前系统日期字符串from_string
返回从字符串转换来的 datetime.date() 值to_string
返回从datetime.date来的日期字符串
>>> from openerp import fields
>>> adate = fields.Date()
>>> fields.Date.today()
'2014-06-15'
>>> fields.Date.context_today(self)
'2014-06-15'
>>> fields.Date.context_today(self, timestamp=datetime.datetime.now())
'2014-06-15'
>>> fields.Date.from_string(fields.Date.today())
datetime.datetime(2014, 6, 15, 19, 32, 17)
>>> fields.Date.to_string(datetime.datetime.today())
'2014-06-15'
日期和时间型(DateTime)¶
存储日期和时间。 这种字段提供一些辅助方法:
context_timestamp
返回基于时区(tz)的当日日期时间戳字符串now
返回当前系统日期和时间字符串from_string
返回从字符串转换来的 datetime.datetime() 值to_string
返回从datetime.date来的日期和时间字符串
>>> fields.Datetime.context_timestamp(self, timestamp=datetime.datetime.now())
datetime.datetime(2014, 6, 15, 21, 26, 1, 248354, tzinfo=<DstTzInfo 'Europe/Brussels' CEST+2:00:00 DST>)
>>> fields.Datetime.now()
'2014-06-15 19:26:13'
>>> fields.Datetime.from_string(fields.Datetime.now())
datetime.datetime(2014, 6, 15, 19, 32, 17)
>>> fields.Datetime.to_string(datetime.datetime.now())
'2014-06-15 19:26:13'
可选型(Selection)¶
存储文本,但给出一个可选项小部件。 它向数据库引入了非可选约束。(It induces no selection constraint in database.) 可选型必须设置为元组(tuples)列表或者一个返回元组(tuples)列表的可调用方法:
aselection = fields.Selection([('a', 'A')])
aselection = fields.Selection(selection=[('a', 'A')])
aselection = fields.Selection(selection='a_function_name')
特殊选项:
- selection: 元组(tuples)列表或者一个使用记录集为输入参数的返回元组(tuples)列表的可调用方法名称
- size: 当使用的索引是整型而非字符串时,必须设定本选项且须设置size=1
在扩展一个模型时,如果你希望向可选型字段添加可能的值,你应该用到 selection_add 关键字参数:
class SomeModel(models.Model):
_inherits = 'some.model'
type = fields.Selection(selection_add=[('b', 'B'), ('c', 'C')])
引用型(Reference)¶
存储到一个模型和一行记录的任意引用:
aref = fields.Reference([('model_name', 'String')])
aref = fields.Reference(selection=[('model_name', 'String')])
aref = fields.Reference(selection='a_function_name')
特殊选项:
- selection: 元组(tuples)列表或者一个使用记录集为输入参数的返回元组(tuples)列表的可调用方法名称
多对一型(Many2one)¶
存储模型之间的多对一关联:
arel_id = fields.Many2one('res.users')
arel_id = fields.Many2one(comodel_name='res.users')
an_other_rel_id = fields.Many2one(comodel_name='res.partner', delegate=True)
特殊选项:
- comodel_name: 对应的模型名称
- delegate: 为了从当前模型访问目标模型的字段,需要将此选项设为
True
(对应于_inherits
)
一对多型(One2many)¶
存储到对应模型的多个记录的关联:
arel_ids = fields.One2many('res.users', 'rel_id')
arel_ids = fields.One2many(comodel_name='res.users', inverse_name='rel_id')
特殊选项:
- comodel_name: 对应的模型名称
- inverse_name: 对应的模型的关联字段名称
多对多型(Many2many)¶
存储到对应模型的多对多个记录的关联:
arel_ids = fields.Many2many('res.users')
arel_ids = fields.Many2many(comodel_name='res.users',
relation='table_name',
column1='col_name',
column2='other_col_name')
特殊选项:
- comodel_name: 对应的模型名称
- relation: 关联的表名称
- columns1: 关联表左字段名称
- columns2: 关联表右字段名称
字段缺省值¶
default 现在是字段的一个关键字,你可以使用值或者方法来为该属性赋值:
name = fields.Char(default='A name')
# 或者
name = fields.Char(default=a_fun)
#...
def a_fun(self):
return self.do_something()
当使用方法时,你必须在字段定义前定义该方法。
计算字段(Computed Fields)¶
不再有直接的 fields.function 创建方式。
作为替代,你可以添加一个 compute
关键字。该关键字属性的值就是一个方法名字符串或者一个返回方法名称的方法。
它允许你在类一开始的部分就定义字段:
class AModel(models.Model):
_name = 'a_name'
computed_total = fields.Float(compute='compute_total')
def compute_total(self):
...
self.computed_total = x
这个方法可以是空的。 它应该修改记录属性以便写入到缓存里:
self.name = new_value
要注意这个赋值会触发数据库的写操作。 如果你需要对大量数据进行修改或者必须考虑性能,你应该使用经典方式来写数据库。
为了提供搜索功能到未持久化的计算字段,你必须为该字段添加 search
关键字。该关键字属性的值就是一个方法名字符串或者或者之前定义的一个返回方法名称的方法,这个方法的第2和第3个参数均为域元组(domain tuple),返回一个域(domain)本身(The function takes the second and third member of a domain tuple and returns a domain itself):
def search_total(self, operator, operand):
...
return domain # e.g. [('id', 'in', ids)]
翻转(Inverse)¶
翻转 inverse
关键字允许在字段写入或“创建”时触发装饰方法。
多字段(Multi Fields)¶
一个方法计算多个值:
@api.multi
@api.depends('field.relation', 'an_otherfield.relation')
def _amount(self):
for x in self:
x.total = an_algo
x.untaxed = an_algo
属性字段(Property Field)¶
在有一些用例里,字段值必须修改到当前公司的依赖。
要启用这种动作,你现在可以使用 company_dependent 选项。
一个值得注意的进展是,在新API里属性字段(property fields)现在是可搜索的。
半成品可拷贝选项(WIP copyable option)¶
在字段上简单的设定 copy
选项,可以防止重新定义拷贝(There is a dev running that will prevent to redefine copy by simply
setting a copy option on fields):
copy=False # !! WIP to prevent redefine copy
方法与装饰器¶
新的装饰器只用于新API。 因为网页客户端(webclient)和HTTP控制器与新API不兼容,所以对新API装饰器是强制使用的。
api
命名空间下的装饰器会检查方法的参数名称以确定是否符合旧参数。
已认定的参数名称有:
cr, cursor, uid, user, user_id, id, ids, context
@api.returns¶
这个装饰器保证返回值的一致性。 它基于原始返回值返回指定模型的一个记录集:
@api.returns('res.partner')
def afun(self):
...
return x # 一个记录集
如果一个旧API方法调用新API方法,它会自动转换为一个id列表。
所有装饰器都继承自这个装饰器,以升级或降级返回值。
@api.one¶
这个装饰器自动遍历记录集的记录,self被重新定义为当前记录:
@api.one
def afun(self):
self.name = 'toto'
注解
注意:返回值被放进一个列表里,这种做法并不是总被网页客户端支持,例如在按钮动作方法里。在那种情况下,你应该用 @api.multi
来装饰你的方法,并且可能需要在方法定义里调用 self.ensure_one() 。
@api.constrains¶
这个装饰器确保当进行创建、写入、删除等操作时,被装饰的方法会被调用。 如果一个约束条件被满足,这个方法应该抛出 openerp.exceptions.Warning 并给出相应警告消息。
@api.depends¶
当这个装饰器指定的任何字段被ORM或表单修改,它所装饰的方法会被触发调用:
@api.depends('name', 'an_other_field')
def afun(self):
pass
注解
当你重新定义依赖时,你必须重新定义所有的 @api.depends,否则它会丢失监视的字段。
视图管理¶
新API的一个重大提升是依赖会通过一种简单的方式自动插入表单,你不用再担心修改视图的事情。
@api.onchange¶
当这个装饰器指定的字段在表单里被修改时,它所装饰的方法会被触发调用:
@api.onchange('fieldx')
def do_stuff(self):
if self.fieldx == x:
self.fieldy = 'toto'
例子里面的 self 对应的记录现在被用户在表单里修改了。
在 on_change 上下文里所有的工作都是在缓存里完成。
所以你可以在你的方法里修改记录集而不用担心修改了数据库记录。
这是跟 @api.depends
的主要区别。
在方法返回时,缓存和记录集之间的差异会被返回给表单(At function return, differences between the cache and the RecordSet will be returned to the form.)。
视图管理¶
新API的一个重大提升是 变动(onchange)会通过一种简单的方式自动插入表单,你不用再担心修改视图的事情。
警告和域(Warning and Domain)¶
要改变域或发送一个警告,返回正常的字典(dictionary)即可。
要小心这种情况下不要使用 @api.one
,因为它会破坏字典(把它放入一个列表,这个不被网页客户端支持)。
@api.noguess¶
这个装饰器阻止新API装饰器去改变一个方法的输出。
自省(Introspection)¶
在OpenERP一个常见模式是使用 _columns
对模型的字段自省。
从8.0版本开始 _columns
已被弃用,而使用 _fields
替代,它包含了使用新、旧API初始化的整理过的字段列表。
约定与代码升级
约定¶
下划线(Snake_casing)还是 驼峰(CamelCasing)¶
目前没有定论。 但是现在看起来 OpenERP SA 在9.0版开始使用驼峰命名方式。
导入(Imports)¶
通过于 Raphaël Collet 的讨论,在8.0 RC1之后将会使用下面的约定
模型(Model)¶
from openerp import models
字段(Fields)¶
from openerp import fields
翻译(Translation)¶
from openerp import _
开发接口(API)¶
from openerp import api
例外(Exceptions)¶
from openerp import exceptions
一个标准的模块导入:n:
from openerp import models, fields, api, _
新的例外类¶
except_orm
例外已经弃用。
我们应该使用 openerp.exceptions.Warning
及其子类实例
注解
不要与Python内建的警告混合。
重定向警告(RedirectWarning)¶
警告用户有可能被重定向而不是简单的显示一个警示消息
应该作为参数接收:
param int action_id: 执行重定向的动作的id param string button_text: 触发重定向的按钮上的文字
禁止访问(AccessDenied)¶
登录、密码错误。无消息,无追溯。
访问错误(AccessError)¶
访问权限错误。
缺失错误(MissingError)¶
记录缺失。
推迟例外(DeferredException)¶
持有对异步报告的追溯的例外对象。
有一些远程调用(RPC)(创建数据库、生成报告)发生时的初始请求伴随着多个或轮询请求。这个类用来存储在该线程处理初始请求时并然后要发送给一个轮询请求时,发生的可能例外。
注解
追溯让人迷惑,这真的是一个 sys.exc_info()
triplet.
字段(Fields)¶
字段应该使用新API的字段声明方式。 使用string关键字来作说明比使用一个长长的属性名称要更好:n:
class AClass(models.Model):
name = fields.Char(string="This is a really long long name") # ok
really_long_long_long_name = fields.Char()
属性名称应该有意义,避免使用类似“nb”这样的名字。
在方法内修改自身¶
我们永远不要在一个模型方法内修改自身。 这种做法会破坏与当前环境缓存的关联性。
在演习(dry run)中执行¶
如果你使用环境上下文管理器的 do_in_draft
,它将只在缓存中执行而不会提交到数据库。
使用记录指针(Cursor)¶
使用记录指针时你应该使用当前环境的记录指针:n:
self.env.cr
except if you need to use threads:
with Environment.manage(): # class function
env = Environment(cr, uid, context)
约束¶
在性能允许的情况下,应该使用 @api.constrains
装饰器与 @api.one
装饰器。
Qweb视图 或 非Qweb视图¶
如果在模型视图里不需要高级的行为,应首先选择标准视图(非Qweb)。
兼容性¶
在迁移期间,有一些保持基础代码在新旧API上兼容性的模式。
单元测试¶
在common.TransactionCase内或其它类里的单元测试中访问新API:n:
class test_partner_firstname(common.TransactionCase):
def setUp(self):
super(test_partner_firstname, self).setUp()
self.user_model = self.env["res.users"]
self.partner_model = self.env["res.partner"]
YAML¶
在Python YAML标签里访问新API:n:
!python {model: account.invoice, id: account_invoice_customer0}: |
self # 现在是新API记录
assert (self.move_id), "Move falsely created at pro-forma"