结构化应用数据
在前面的章节中,我们基本接触了建造Odoo后台应用的所有层面。下面我们就要具体分析‘model,views,business logic’这些组成Odoo应用的不同层面的细节。
首先我们来学习如何设计支持一个应用的数据的结构组成以及如何表达这些数据之间的联系。
组织应用的功能到模块中
首先,我们需要使用一个例子来帮我们理解这个概念。
Odoo的继承功能提供了有效的拓展机制。它允许我们不直接修改源代码来拓展已经存在的第三方应用。这个可组合性还确保了面向对象的开发模式,大的应用能够被分成各种小功能。这种避免复杂性的方法从技术角度跟用户体验角度看来都是非常有用的。所以我们通过添加额外的功能模块来改进我们的To-Do 应用。最后让它变成一个具有丰富功能的大应用。
介绍我们将要创建的 todo_ui 模块
在前面的章节中,我们首先创建了一个个人版的to-do task模块,然后通过继承把它扩展为多用户可以共享的应用。
现在,我们想要提升用户界面,增加一个看板板块,看板板块是一个简单的工作流工具,它把主题放在一个个列中,我们可以把这些主题从左边的列一步步传入右边的列,知道它们完成了整个工作流。现在,我们组织我们的任务放入以Waiting,Ready,Started,Done这样的阶段(Stages)为列名的视图中。
我们首先通过添加数据结构来保证这个视觉效果的底层实现。添加阶段(Stages),让这个阶段模型支持标签,以便我们能够根据主题来对tasks进行分类。我们在本章中只关注模型的数据结构。关于用户界面的讨论会放到下一章来讲解。
在设计支持模型时,首要的事就是理解数据的结构组成。
我们现在已经有了最中心的模型实体—— To-do Task
然后需要创建一个Stage模型跟一个tag模型。它们与我们的task模型之间有如下关系:
- 一个task任务只能在一个stage中,而一个stage里可以同时存在多个task任务 ‘Many2one’
- 一个task任务能有许多tags,而一个标签也能匹配到多个tasks ‘Many2Many’
下面通过创建新的‘todo_ui’模块来添加 To-do Stages 跟To-do Tags 模型。还是用'custom-addons'文件夹来存放我们的'todo_ui'
mkdir todo_ui
cd todo_ui
touch _init__.py
vim __manifest__.py
添加下面的内容
{
'name': 'User interface improvements tp the To-Do app',
'description': 'User friendly features.',
'author': 'xer',
'depends': ['todo_user'],
}
在Odoo中安装这个新模块
创建新模型
为了让我们的to-do tasks拥有看板板块,我们需要使用Stages。Stages 代表了板块中的列。每个task都会填充到这些列里面。
- 编辑
__init__.py
来导入models
包
`from . import models - 创建
todo_ui/models文件夹,在其下添加
init.py,让它变为一个python包文件.
from . import todo_model` - 然后创建
todo_model.py
来定义我们的新模型
from odoo import api,models,fields
class Tag(models.Model):
_name = 'todo.task.tag'
_description = 'To-do Tag'
name = fields.Char('Name',40, translate=True)
class Stage(models.Model):
_name = 'todo.task.stage'
_description = 'To-do Stage'
_order = 'sequence,name'
name = fields.Char('Name',40, translate=True)
sequence = fields.Integer('Sequence')
上面的代码我们定义2个与to-do tasks相关联的新模型。
注意到 task stages, 我们有个继承models.Model
的python类==>Stage
.这定义了一个新的名为todo.task.stage
的Odoo模型.
模型属性
模型类中可以使用附加的属性来控制模型的行为,下面是那些最常使用的属性:
-
_name
:是一个用来标识我们新创建的Odoo模型的标识符 -
_description
: 不是必须的,它表示用户在用户界面看到的用来描述我们模型的标题记录. -
_order
: 设置了当模型的记录被浏览时的默认顺序。就是在底层数据库对模型中的记录排序时添加了一条order by
的SQL子句。 -
_rec_name
: 在关联关系中,指定了用来描述记录的字段.默认在Many2one
关系中使用了name
字段的值显示记录 -
_table
: Odoo数据库会自动生成相关的数据表名来存放我们定义的模型,默认是通过把.
改为_
来作为数据库表名的,通过这个属性可以自定义数据库表名. -
_inherit
跟_inherits
属性.作为继承用法.详请参见第三章.
模型跟Python类.
Odoo模型被存放在中心注册处,在老版本的Odoo中,可以通过pool
来获取到.pool
是一个字典,通过模型的名称保存着与所有Odoo模型实例之间的联系.我们可以使用self.env['x']
来获取到‘x’这个模型代表类。
模型的名称是十分重要的因为它们用作键来获取在中心注册处注册过的模型。模型的起名规范是使用一个以.
为分隔符的小写字符串,例如todo.task.stage
.
Python类的命名并无严格限制,符合PEP8,按照驼峰命名法即可.
临时跟抽象模型
在大多数情况中,我们编写的python模型类都是继承自Odoo的models.Model
.Odoo另外提供了2个模型
- Transient models:这个模型基于
models.TransientModel
类,一般使用在导航形式的用户体验类中.临时模型的数据是临时存储在数据库中的.旧的数据会被新的数据直接取代。(这是提高效率的做法)举例来说,Settings | Translation | Load a Language对话窗口,使用了临时模型来存储用户的语言选择。 - Abstract models:基于
models.AbstractModel
.它没有存储数据。在Odoo继承机制中作为一个混合类来增加新的功能。例如我们使用过的mail.thread
模型.
检查已存在的模型
打开 Settings |Technical | Database Structure | Models。这里保存了所有的‘models’。
创建字段
Oodd中的字段主要分为基础字段跟关系型字段
-
基础字段类型
我们在我们刚创立的‘Stage’模型中添加如下字段
class Stage(models.Model):
_name = 'todo.task.stage'
_description = 'To-do Stage'
_order = 'sequence,name'
name = fields.Char('Name')
desc = fields.Text('Description')
state = fields.Selection([('draft', 'New'), ('open', 'Started'), ('done', 'Closed')], 'State')
docs = fields.Html('Documentation')
sequence = fields.Integer('Sequence')
perc_complete = fields.Float('% Complete', (3, 2))
date_effective = fields.Date('Effective Date')
date_changed = fields.Date('Last Changed')
fold = fields.Boolean('Folded?')
image = fields.Binary('Image')
- Char : 字符型,size属性定义长度
- Text :文本型,没有长度限制
- Selection : 下拉列表,在继承中,可以使用
selection_add
来扩展已经存在的下拉列表 - Html : 文本型,在用户层面可以对其做特殊处理
- Integer:整数型
- Float: 浮点型,属性digits是一个元组(x,y)。x定义总长度,y定义小数部分位数
- Date:短日期,年月日,在vies层通过日历选择框显示。
- Datetime :时间戳
- Boolean : 布尔类型
- Binary :二进制数据,在视图层显示为一个文件上传按钮,可以把图片,音频,视频,文档以二进制形式保存。
字段常见属性:
- string :在视图层运用的字段的标注,是第一个可选参数,所有一般可以直接传值,不需要使用字典参数。
- defalut :设置了一个字段默认值,可以是一个固定的值,一个引用,一个函数名甚至一个简单的lambda函数
- translate: 只在Char,Text,Html字段中使用。进行翻译。
- help:在用户界面显示帮助信息
- readonly=True:设置只读属性。
- required=True :必须属性,相当于在数据库中添加了NOT NULL的限制
- index=True:创建索引
- copy=False:默认的非关系型字段都是可以复制的
- groups:设置权限,使用groups的XML IDs作为值。
- states:一个字典映射关系,依赖state字段的值,能使用的属性为
readonly
,required
,invisible
举例:
states = {'done':[('readonly',True)]}
特殊名字的字段
- Odoo自动生成的字段名
- id: 自动生成的数字标识,作为数据库主键
- create_uid: 创建用户
- create_data: 创建日期
- write_uid: 修改用户
- write_date: 修改日期
- Odoo中避免使用的字段名
- name:默认用来展示模型名称的。通常是Char字段。我们可以通过_rec_name这个模型属性来使用其他字段作为显示
- Active:是一个Boolean字段,用来在视图层展示记录。
- Sequence: Integer字段,在试图层进行排序用
- State: Selection字段,代表了记录的状态。可以用来动态的改变视图层的展示
- parent_id, parent_left,parent_right: Integer字段,在父子继承关系中有特殊意义.
-
关系型字段
以我们的模块为例,我们有如下的模型之间的关系:
- 每个任务有一个阶段: 多对一关系
-
每个任务有多个标签: 多对多关系
添加代码:
class TodoTask(models.Model):
_inherit = 'todo.task'
stage_id = fields.Many2one('todo.task.stage', 'Stage')
tag_ids = fields.Many2many('todo.task.tag', string='Tags')
Many-to-one 关系:
Many2one的主要属性:
- comodel:关联的其他模型名称
- string: 字符串名称
- ondelete: 定义了当关联模型记录被删除后的操作。默认是
set null
操作。restrict
表示抛异常,防止误删,cascade
表示本记录也一同删除. - context: 是一个字典,表示在视图层传递信息。通常可以用来设置默认值。
- domain:domain规则进行过滤。
- aut_join=True: 允许使用SQL层面的join子句.
Many-to-many关系:
创立了第三张表来表示两个多对多关系模型之间的具体映射,中间表的命名是自动的,规则是两张模型表名之后加入'_rel'.但是当表名太长超过psql规定的63个字符时,我们需要自己来定义表名.
我们有2中方法定义Many2many字段.通常使用字典参数,因为这样容易理解。举例:
Task跟Tag模型之间创立关系:
在Task模型中.
tag_ids = fields.Many2many(
comodel_name='todo.task.tag',
relation='todo_task_tag_rel',
column1='task_id',
column2='tag_id',
string='Tags')
在Tag模型中.
task_ids = fields.Many2many('todo.task',string='Tasks')
One-to-many 关系。
Odoo中的One2many不能单独存在, 是于Many2one相对应的。实际上并没什么意义,但是对于反向遍历多对一关系时很有用处。
在Stage模型中,我们这样定义一对多的存在:
tasks = fields.One2many('todo.task', 'stage_id', 'Tasks in this stage')
One2many字段接收3个可选参数:
- 关联模型----'todo.task'
- 关联模型中的相关字段-----'stage_id' 表示与task中的'stage_id'字段互相对于
- 字符串标题名称
分层关系
父子树关系结构也可以看做是Many2one
关联字段使用动态关系
在task模型中
如下:
refers_to = fields.Reference([('res.user', 'User'), ('res.partner', 'Partner')], 'Refers to')
我们可以直接通过Reference字段来让task跟‘User’,‘Partner’建立关联。Reference字段的定义与Selection比较相似。
计算字段
字段的值可以通过函数计算得到。计算字段的定义很简单,跟普通字段一样,只是在属性中添加一个compute
属性.
举例:
Stages模型中有一个fold字段,这个字段是一个布尔值,用来标识是否折叠。在我们的TodoTask模型中对它的值进行计算获取。
stage_fold = fields.Boolean('Stage Folded?', compute='_compute_stage_fold',)
@api.depends('stage_id.fold')
def _compute_stage_fold(self):
for task in self:
task.stage_fold = task.stage_id.fold
上面的代码在Task模型中添加了一个stage_fold字段,并且使用了_compute_stage_fold这个函数来对其计算取值.
@api.depends装饰器中需要放入我们计算取值需要的字段.可以认为是计算函数需要的参数.
搜索跟编辑计算字段
我们刚才创建的计算字段并不能被搜索或者写入数据,只是一个可读字段.为了增加这两个操作.我们使用
- search : 为计算字段进行搜索
- inverse : 为计算字段进行写入数据.
stage_fold = fields.Boolean('Stage Folded?', compute='_compute_stage_fold',
search='_search_stage_fold',
inverse='_write_stage_fold'
)
def _search_stage_fold(self, operator, value):
return [('stage_id.fold', operator, value)]
def _write_stage_fold(self):
self.stage_id.fold = self.stage_fold
上面代码中,search代表的函数表示该字段可以被搜索。inverse代表的函数表示改变stage_fold的值可以反向写入到stage_id.fold字段中.
存储计算字段.
在计算字段定义时添加属性store = True
即可.当计算字段所依赖的数据改变时,它也会相应的进行更新.
依赖字段
刚才我们定义的计算字段其实就是在Task模型中把Stage的fold字段给复制了出来.我们使用依赖字段也可以实现这一操作.
定义这样的字段跟普通字段一样,唯一不同就是添加一个related属性,related属性的值通过'.'操作符来直接获取我们需要的目标字段.
stage_state = fields.Selection(related='stage_id.state', string='Stage State',store=True, inverse='_write_state')
就像上面的代码。我们直接在Task模型中定义了依赖字段。直接获取到了Stage中的state值。
模型限制。
为了防止非法数据,Odoo支持2种模型数据限制:
- SQL限制:SQL限制的原理是通过Psql数据库的数据库表定义来防止非法数据的产生。
使用_sql_constraints
这个类属性来进行定义。
它是一个元组列表,格式为 限制名称,SQL限制条件,错误信息
- Python限制:
我们使用@api.constraints装饰器来对需要限制的字段进行规范限制。
举例,我们的Task名字至少是要5个字符:
@api.constrains('name')
def _check_name_size(self):
for task in self:
if len(task.name)< 5:
raise ValidationError('Must have 5 chars!')
上面的代码表示当task的名字小于5个字符时,会抛出状态异常。Odoo会直接在视图界面发出警告。