计算字段和默认值
到目前为止,我们接触的字段都是存储在数据库中并直接从数据库检索。字段也可以通过计算获得。在这种情况下,字段的值不是直接检索自数据库,而是通过调用模型的方法来实时计算获得。要创建计算字段,需要设置它的compute
属性为方法名。这个计算方法通过计算self
的每条记录来设置字段的值。
注意
self
是一个记录的有序集合,它支持标准的Python集合操作,如len(self)
和iter(self)
,加上额外的集合操作recs1 + recs2
。迭代过程逐个提供self
记录,其中每个记录本身是大小为1的集合。你可以通过点记号来访问/分配单个记录上的字段record.name
。
import random
from odoo import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
@api.multi
def _compute_name(self):
for record in self:
record.name = str(random.randint(1, 1e6))
依赖
计算字段的值通常取决于所在记录行的其它字段的值。ORM层期望开发人员使用depends()
装饰器来指定计算方法的依赖性。当某些依赖关系被修改后,ORM层通过给定的依赖关系来触发字段的重新计算。
from odoo import models, fields, api
class ComputedModel(models.Model):
_name = 'test.computed'
name = fields.Char(compute='_compute_name')
value = fields.Integer()
@api.depends('value')
def _compute_name(self):
for record in self:
record.name = "Record with value %s" % record.value
练习计算字段
- 加入座席占用百分比字段到授课模型。
- 在列表视图和表单视图中显示这个字段
- 以进度条的方式显示这个字段
openacademy/models.py
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
taken_seats = fields.Float(string="Taken seats", compute='_taken_seats')
@api.depends('seats', 'attendee_ids')
def _taken_seats(self):
for r in self:
if not r.seats:
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
openacademy/views/openacademy.xml
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
<field name="taken_seats" widget="progressbar"/>
</group>
</group>
<label for="attendee_ids"/>
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
<field name="taken_seats" widget="progressbar"/>
</tree>
</field>
</record>
默认值
任何字段都可以给出默认值。在字段定义中,添加选项default=x
,x可以是Python字面值(bool,int,float,string),也可以是一个有返回值的方法。
name = fields.Char(default="Unknown")
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
注意
self.env
对象给出了访问请求参数和其他有用的信息:
-
self.env.cr
或者self._cr
是数据库游标对象,通常用于查询数据库 -
self.env.uid
或者self._uid
是当前用户的数据库ID -
self.env.user
是当前用户记录 -
self.env.ref(xml_id)
返回XML ID对应的记录 -
self.env[model_name]
返回给定模型的实例
练习默认值
- 定义start_date默认值为今天
- 在授课类添加字段
active
,并且设置其默认值为True
openacademy/models.py
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date(default=fields.Date.today)
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
active = fields.Boolean(default=True)
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
openacademy/views/openacademy.xml
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
<field name="active"/>
</group>
<group string="Schedule">
<field name="start_date"/>
注意
Odoo 有内置规则:active
字段值为False
时记录不可见
Onchange机制
"onchange"机制为客户端界面提供了一种方法,当用户在字段中填写了值,不需要向数据库保存任何内容,就可以更新表单。例如,假设模型有三个字段amount
,unit_price
和price
,当数量和单价改变时,自动重新计算价格,并在表单界面更新。要实现这个需求,需要定义一个方法,并使用onchange()
装饰器,onchange()
的参数指定了在那个字段改变时,触发方法。其中self
代表表单视图中的记录,你所做的任何更改,self
都将立刻反应在表单上。
<!-- content of form view -->
<field name="amount"/>
<field name="unit_price"/>
<field name="price" readonly="1"/>
# onchange handler
@api.onchange('amount', 'unit_price')
def _onchange_price(self):
# set auto-changing field
self.price = self.amount * self.unit_price
# Can optionally return a warning and domains
return {
'warning': {
'title': "Something bad happened",
'message': "It was very bad indeed",
}
}
对于计算字段的值,系统内置了onchange()
装饰器。通过授课表单我们可以观察到:当改变座席数和参与者人数,taken_seats
进度条会自动更新。
练习
通过"onchange"机制显示的验证无效值,例如座位数为负数或者座位数多与参与者。
openacademy/models.py
r.taken_seats = 0.0
else:
r.taken_seats = 100.0 * len(r.attendee_ids) / r.seats
@api.onchange('seats', 'attendee_ids')
def _verify_valid_seats(self):
if self.seats < 0:
return {
'warning': {
'title': "Incorrect 'seats' value",
'message': "The number of available seats may not be negative",
},
}
if self.seats < len(self.attendee_ids):
return {
'warning': {
'title': "Too many attendees",
'message': "Increase seats or remove excess attendees",
},
}
模型约束
odoo提供两种方式实现自动验证,python constraints
和sql constraints
Python约束通过方法装饰器constraints()
来定义,并在记录集上调用这个方法。装饰器参数指定了约束涉及的字段,当涉及的字段中任一发生改变时触发方法执行。如果不满足约束条件,该方法将引发异常:
from odoo.exceptions import ValidationError
@api.constrains('age')
def _check_something(self):
for record in self:
if record.age > 20:
raise ValidationError("Your record is too old: %s" % record.age)
# all records passed the test, don't return anything
练习添加Python约束,讲师不能在自己的授课出席人中
openacademy/models.py
# -*- coding: utf-8 -*-
from odoo import models, fields, api, exceptions
class Course(models.Model):
_name = 'openacademy.course'
'message': "Increase seats or remove excess attendees",
},
}
@api.constrains('instructor_id', 'attendee_ids')
def _check_instructor_not_in_attendees(self):
for r in self:
if r.instructor_id and r.instructor_id in r.attendee_ids:
raise exceptions.ValidationError("A session's instructor can't be an attendee")
SQL约束通过模型属性_sql_constraints
进行定义。它是一个三元素的元组的列表(name, sql_definition, message),其中name
是SQL约束名称,sql_definition
是约束规则,message
是违反约束规则时的警告信息。
练习添加SQL约束,在Postgre SQL文档帮助下,添加下列约束:
- 验证课程描述与课程标题不能完全一样
- 验证课程名是唯一的
openacademy/models.py
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',
"The title of the course should not be the description"),
('name_unique',
'UNIQUE(name)',
"The course title must be unique"),
]
class Session(models.Model):
_name = 'openacademy.session'
练习添加重复项,因为我们为课程名称添加了唯一性约束,所以不能再使用"复制"功能(表单->复制)。重写"复制"方法,允许复制课程对象,将原始名称更改为"原始名称的副本"。
openacademy/models.py
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
@api.multi
def copy(self, default=None):
default = dict(default or {})
copied_count = self.search_count(
[('name', '=like', u"Copy of {}%".format(self.name))])
if not copied_count:
new_name = u"Copy of {}".format(self.name)
else:
new_name = u"Copy of {} ({})".format(self.name, copied_count)
default['name'] = new_name
return super(Course, self).copy(default)
_sql_constraints = [
('name_description_check',
'CHECK(name != description)',