Chapter 5 Models – Structuring the Application Data

第五章-结构化应用数据


In the previous chapters, we had an end-to-end overview of creating new modules for Odoo. In Chapter 2, Building Your First Odoo Application, a completely new application was built, and in Chapter 3, Inheritance – Extending Existing Applications, we explored inheritance and how to use it to create an extension module for our application. In Chapter 4, Data Serialization and Module Data, we discussed how to add initial and demonstration data to our modules.

在前一章,我们从头到尾地了解了Odoo的新模块创建。在第二章——构建你的第一个Odoo应用,构建了一个全新的应用,在第三章,继承-扩展现有应用,我们浏览了继承,以及如何使用它为应用创建一个扩展模块。在第四章,数据序列化和模块数据,我们讨论了如何对模块添加初始示例数据。

In these overviews, we touched all the layers involved in building a backend application for Odoo. Now, in the following chapters, it’s time to explain in more detail these several layers making up an application: models, views, and business logic.

通过这些概要,我们接触到了构建Odoo后端应用所涉及到全部层面。现在,下面的章节,就是用来详细解释构建一个的几个层面:模型,视图,以及业务逻辑。

In this chapter, you will learn how to design the data structures supporting an application, and how to represent the relations between them.

在这一章,你会学习到如何设计支持一个应用的数据结构,以及如何表现两者之间的关系。

Organizing application features into modules 将应用功能组织到模块

As before, we will use an example to help explain the concepts. One of the great things about Odoo is being able to pick any existing application or module and add, on top of it, those extra features you need. So we are going to continue improving our to-do modules, and in no time they will form a fully featured application!

就像之前那样,我们举例来说明概念。Odoo的其中一个强大的之处在于它能够选择任意现有的应用或者模块,然后在其之上,添加你需要的额外功能。所以我们要继续改进to-do模块,不需要太久的时间它们就可以形成一个全功能的应用了!

It is a good practice to split Odoo applications into several smaller modules, each of them responsible for specific features. This reduces overall complexity and makes them easier to maintain and upgrade to later Odoo versions. The problem of having to install all these individual modules can be solved by providing an app module packaging all those features, through its dependencies. To illustrate this approach we will be implementing the additional features using new to-do modules.

将Odoo应用分割到多个较小的模块就是一个最佳实践,每个模块各自负责特定的功能。这样可以减少整体上的复杂性,而且让这些模块便于维护,以及升级到新版本Odoo。安装这些独立模块的问题可以通过提供一个打包了全部功能和依赖的应用模块来解决。为了说明这个方法,我们

Introducing the todo_ui module 介绍todo_ui模块

In the previous chapter, we first created an app for personal to-dos, and then extended it so that the to-dos could be shared with other people.

在前面以后做那个,我们首先创建了一个人的todo应用,然后扩展它,以便能够与其它人共同使用。

Now we want to take our app to a new level by adding to it a kanban board and a few other nice user interface improvements. The kanban board will let us organize our tasks in columns, according to their stages, such as Waiting, Ready, Started or Done.

现在,我们想要

We will start by adding the data structures to enable that vision. We need to add stages and it will be nice if we add support for tags as well, allowing the tasks to be categorized by subject.

我们从添加数据结构实现愿景开始。我们需要添加

The first thing to figure out is how our data will be structured so that we can design the supporting Models. We already have the central entity: the to-do task. Each task will be in a stage, and tasks can also have one or more tags on them. This means we will need to add these two additional models, and they will have these relations:

第一件要弄明白的事情是我们数据将如何构建,这样我们能够设计支持的模型。我们已经拥有中心模型了:to-do task。每个任务

  • Each task has a stage, and there can be many tasks in each stage.
  • Each task can have many tags, and each tag can be in many tasks.

每个任务都有一个阶段,而且在每个阶段中也可以拥有许多的任务。
每个任务拥有很多的标签,每个标签可以存在于很多的任务重。

This means that tasks have many to one relation with stages, and many to many relations with tags. On the other hand, the inverse relations are: stages have a one to many relationship with tasks and tags have a many to many relation with tasks.

这就意味着,任务与阶段存在多对一段关系,和标签存在多对多关系。换句话来说,反向关系就是:阶段与任务存在一对多点关系,而标签和任务之间存在多对多关系。

We will start by creating the new todo_ui module and add the to-do stages and to-do tags models to it.

我们从创建新的todo_ui模块开始,然后把to-do的stage模型和to-do的标签模型添加到这个模块。

We’ve been using the ~/odoo-dev/custom-addons/ directory to host our modules. To create the new module alongside the existing ones, we can use these shell commands:

我们已经使用~/odoo-dev/custom-addons/目录来托管自定义的模块。为了在现有模块的

  1. $ cd ~/odoo-dev/custom-addons
  2. $ mkdir todo_ui
  3. $ cd todo_ui
  4. $ touch todo_model.py
  5. $ echo "from . import todo_model" > __init__.py

Next, we should add the __openerp__.py manifest file with this content:

接下来,我们应该把下面的内容添加到__openerp__.py清单文件:

  1. { 'name': 'User interface improvements to the To-Do app',
  2. 'description': 'User friendly features.',
  3. 'author': 'Daniel Reis',
  4. 'depends': ['todo_app'] }

Note that we are depending on todo_app and not on todo_user. In general, it is a good idea to keep modules as independent as possible. When an upstream module is changed, it can impact all other modules that directly or indirectly depend on it. It’s best if we can keep the number of dependencies low, and also avoid long dependency stacks, such as todo_ui → todo_user → todo_app in this case.

注意我们依赖于todo_app而不是todo_user。通常,

Now we can install the module in our Odoo work database and get started with the models.

现在我们可以在Odoo工作数据库中安装模块,

Creating models 创建模型

For the to-do tasks to have a kanban board, we need stages. Stages are the board columns, and each task will t into one of these columns.

对于to-do tasks拥有一个看板视图来说我们需平台。平台是看板点列,

Let’s add the following code to the todo_ui/todo_model.py file:

让我们添加下面的代码到todo_ui/todo_model.py文件:

  1. # -*- coding: utf-8 -*-
  2. from openerp import models, fields, api
  3. class Tag(models.Model):
  4. _name = 'todo.task.tag'
  5. name = fields.Char('Name', 40, translate=True)
  6. class Stage(models.Model):
  7. _name = 'todo.task.stage'
  8. _order = 'sequence,name'
  9. _rec_name = 'name' # the default
  10. _table = 'todo_task_stage' # the default
  11. name = fields.Char('Name', 40, translate=True)
  12. sequence = fields.Integer('Sequence')

Here, we created the two new Models we will be referencing in the to-do tasks.

这里,我们创建了两个会在to-do tasks中引用的新模型。

Focusing on the task stages, we have a Python class, Stage, based on the class models.Model, defining a new Odoo model, todo.task.stage. We also defined two fields, name and sequence. We can see some model attributes, (prefixed with an underscore) that are new to us. Let’s have a closer look at them.

我们把注意力集中在task的阶段上,我们拥有一个基于models.Model类的Python类Stage,它定义了一个新的Odoo模型,todo.task.stage。我们也定义了两个字段,name和sequence。我们可以看到一些模型属性,

Model attributes 模型属性

Model classes can have additional attributes used to control some of their behaviors:

模型类可以拥有用来控制自身行为的额外属性:

  • _name: This is the internal identifier for the Odoo model we are creating.
  • _order: This sets the order to use when the model’s records are browsed. It is a text string to be used as the SQL order by clause, so it can be anything you could use there.
  • _rec_name: This indicates the field to use as the record description when referenced from related fields, such as a many to one relation. By default, it uses the name field, which is a commonly found field in models. But this attribute allows us to use any other field for that purpose.
  • _table: This is the name of the database table supporting the model. Usually, it is left to be calculated automatically, and is the model name with the dots replaced by underscores. But it can be set to indicate a specific table name.

  • _name

For completeness, we can also have the _inherit and _inherits attributes, as explained in Chapter 3, Inheritance - Extending Existing Applications.

Models and Python classes 模型Python类

Odoo models are represented by Python classes. In the preceding code, we have a Python class Stage, based on the models.Model class, used to define a new Odoo model todo.task.stage.

Odoo模型由Python类表示,。在之前的代码中,我们编写了Python类Stage,它基于被用来定义一个新的Odoo模型的todo.task.stage的models.Model类。

Odoo models are kept in a central registry, also referred to as pool in the previous versions. It is a dictionary keeping references of all the model classes available in the instance, and can be referenced by model name. Speci cally, the code in a model method can use self.envl[‘x’] or self.env.get(‘x’) to get a reference to a class representing model x.

Odoo模型被保存在一个注册中心里,在之前的版本中也被引用为pool。

You can see that model names are important since they are the key used to access
the registry. The convention for model names is to use a list of lowercase words joined with dots, like todo.task.stage. Other examples from the core modules are project.project, project.task or project.task.type. We should use the singular form: todo.task instead of todo.tasks. For historical reasons it’s possible to nd some core models not following this, such as res.users, but that is not the rule.

Model names must be globally unique. Because of this, the first word should correspond to the main application the module relates to. In our example, it is todo. Other examples from the core modules are project, crm, or sale.

Python classes, on the other hand, are local to the Python file where they are declared. The identi er used for them is only signi cant for the code in that file.

Because of this, class identi ers are not required to be pre xed by the main application they relate to. For example, there is no problem to call just Stage to our class for the todo.task.stage model. There is no risk of collision with possible classes with the same name on other modules.

Two different conventions for class identi ers can be used: snake_case or CamelCase. Historically, Odoo code used snake case, and it is still very frequent to nd classes using that convention. But the recent trend is to use camel case, since it is the Python standard de ned by the PEP8 coding conventions. You may have noticed that we are using the latter form.

Transient and Abstract models 临时模型和抽象模型

In the preceding code, and in the vast majority of Odoo models, classes are based on the models.Model class. This type of models have database persistence: database tables are created for them and their records are stored until explicitly deleted.

在前面的代码中,以及非常多的Odoo模型中,类都基于models.Model类。

But Odoo also provides two other model types to be used: Transient and Abstract models.

不过Odoo也提供了其它两种类型的模型来使用:临时模型和抽象模型。

Transient models are based on the models.TransientModel class and are used for wizard-style user interaction. Their data is still stored in the database, but it is expected to be temporary. A vacuum job periodically clears old data from these tables.

临时模型基于类models.TransientModel,它被用在wizard风格的用户交互之上。它们的数据仍旧是存储在数据库中的,

Abstract models are based on the models.AbstractModel class and have no data storage attached to them. They act as reusable feature sets to be mixed in with other models. This is done using the Odoo inheritance capabilities.

Inspecting existing models 检查现有模型

The information about models and elds created with Python classes is available through the user interface. In the Settings top menu, select the Technical | Database Structure | Models menu item. Here, you will nd the list of all models available in the database. Clicking on a model in the list will open a form with its details.

img:omit

This is a good tool to inspect the structure of a Model, since you have in one place the result of all additions that may come from several different modules. In this case, as you can see at the In Modules eld, on the top right, the todo.task de nitions are coming from the todo_app and todo_user modules.

In the lower area, we have some information tabs available: a quick reference for the model Fields, the Access Rights granted, and also list the Views available for this model.

We can nd the model’s External Identi er, by activating the Developer Menu and accessing its View Metadata option. These are automatically generated but fairly predictable: for the todo.task model, the External Identi er is model_todo_task.

Tip

The Models form is editable! It’s possible to create and modify models, elds, and views from here. You can use this to build prototypes before carving them into proper modules.

Creating fields 创建字段

After creating a new model, the next step is to add fields to it. Let’s explore the several types of fields available in Odoo.

在创建一个新模型之后,下一步就是对这个模型添加字段。我们来浏览一下Odoo中可以使用的几种字段类型。

Basic field types 基本字段类型

We now have a Stage model and will expand it to add some additional elds. We should edit the todo_ui/todo_model.py le, by removing some unnecessary attributes included before for the purpose of explanation, making it look like this:

  1. class Stage(models.Model):
  2. _name = 'todo.task.stage'
  3. _order = 'sequence,name'
  4. # String fields:
  5. name = fields.Char('Name', 40)
  6. desc = fields.Text('Description')
  7. state = fields.Selection(
  8. [('draft','New'), ('open','Started'),('done','Closed')],
  9. 'State')
  10. docs = fields.Html('Documentation')
  11. # Numeric fields:
  12. sequence = fields.Integer('Sequence')
  13. perc_complete = fields.Float('% Complete', (3, 2))
  14. # Date fields:
  15. date_effective = fields.Date('Effective Date')
  16. date_changed = fields.Datetime('Last Changed')
  17. # Other fields:
  18. fold = fields.Boolean('Folded?')
  19. image = fields.Binary('Image')

Here, we have a sample of the non-relational field types available in Odoo, with the basic arguments expected by each function. For most, the first argument is the field title, corresponding to the string keyword attribute. It’s an optional argument, but it is recommended to be provided. If not, a title will be automatically generated from the eld name.

There is a convention for date fields to use date as a prefix in their name. For example, we should use dateeffective instead of effective_date. This can also apply to other elds, such as amount, price or qty.

A few more arguments are available for most field types:

  • Char accepts a second, optional argument, size, corresponding to the maximum text size. It’s recommended to use it only if you have a good reason to.
  • Text differs from Char in that it can hold multiline text content, but expects the same arguments.
  • Selection is a drop-down selection list. The first argument is the list of selectable options and the second is the title string. The selection list items are (‘value’, ‘Title’) tuples for the value stored in the database and the corresponding description string. When extending through inheritance, the selection_add argument can be used to append items to an existing selection list.
  • Html is stored as a text field, but has specific handling to present HTML content on the user interface.
  • Integer just expects a string argument for the field title.
  • Float has a second optional argument, an (x,y) tuple with the field’s
    precision: x is the total number of digits; of those, y are decimal digits.
  • Date and Datetime data is stored in UTC time. There are automatic conversions made, based on the user time zone preferences, made available through the user session context. This is discussed in more detail in Chapter 6, Views – Designing the User Interface.
  • Boolean only expects the field title to be set, even if it is optional.
  • Binary also expects only a title argument.
    Other than these, we also have the relational elds, which will be introduced later in this chapter. But now, there is still more to learn about these eld types and their attributes.

Common field attributes 常见字段属性

Fields also have a set of attributes we can use, and we’ll explain these in more detail:

  • string is the field title, used as its label in the UI. Most of the time it is not used as a keyword argument, since it can be set as a positional argument.
  • default sets a default value for the field. It can be a static value or a callable, either a function reference or a lambda expression.
  • size applies only to Char fields, and can set a maximum size allowed.
  • translate applies to text fields, Char, Text and Html, and makes the field
    translatable: it can have different values for different languages.
  • help provides the text for tooltips displayed to the users.
  • readonly=True makes the field not editable on the user interface.
  • required=True makes the field mandatory.
  • index=True will create a database index on the field.
  • copy=False has the field ignored when using the copy function. The non-relational fields are copyable by default.
  • groups allows limiting the field’s access and visibility to only some groups. It is a comma-separated list of strings for security group XML IDs.
  • states expects a dictionary mapping values for UI attributes depending on values of the state field. For example: states={‘done’:[(‘readonly’,True) ]}. Attributes that can be used are readonly, required, and invisible.

For completeness, two other attributes are sometimes used when upgrading between Odoo major versions:

  • deprecated=True logs a warning whenever the field is being used.
  • oldname=’field’ is used when a field is renamed in a newer version,
    enabling the data in the old field to be automatically copied into the new field.

Reserved field names 保留字段名

A few field names are reserved to be used by the ORM:
一些字段的名称是被ORM保留使用的:

  • id is an automatic number uniquely identifying each record, and used as the database primary key. It’s automatically added to every model.

The following fields are automatically created on new models, unless the _log_ access=False model attribute is set:

下列的字段在新模型上能够自动的创建,除非设置了模型属性_log_ access=False

  • create_uid for the user that created the record
  • create_date for the date and time when the record is created
  • write_uid for the last user to modify the record
  • write_date for the last date and time when the record was modified

This information is available from the web client, using the Developer Mode menu and selecting the View Metadata option.

这条信息也可以在web客户端看到,使用开发者模式菜单然后选择“查看元数据”选项。

There some built-in effects that expect specific field names. We should avoid using them for purposes other than the intended ones. Some of them are even reserved and can’t be used for other purposes at all:

希望指定的字段名会带来一些内置效应。我们应该避免将这这些字段用于多种目的。这些字段中的一部分是保留的

  • name is used by default as the display name for the record. Usually it is a Char, but other field types are also allowed. It can be overridden by setting the _rec_name model attribute.

  • name 在显示记录名称时它被默认使用。通常是一个Char,不过其他字段类型也是被允许的。

  • active (type Boolean) allows inactivating records. Records with active==False will automatically be excluded from queries. To access them an (‘active’,’=’,False) condition must be added to the search domain, or ‘active_test’: False should be added to the current context.

  • active (布尔类型)能够讲记录失效。含有active==False的记录可以自动的在查询时被排除。要访问

  • sequence (type Integer) if present in a list view, allows to manually define the order of the records. To work properly it should also be in the model’s _order.

  • sequence (整数类型)如果它出现在列表视图中,

  • state (type Selection) represents basic states of the record’s life cycle, and can be used by the state’s field attribute to dynamically modify the view: some form fields can be made read only, required or invisible in specific record states.

  • state (多选类型)表示记录的基本状态

  • parent_id, parent_left, and parent_right have special meaning for parent/child hierarchical relations. We will shortly discuss them in detail.

So far we’ve discussed scalar value fields. But a good part of an application data structure is about describing the relationships between entities. Let’s look at that now.

到目前为止我们讨论了无向量值的字段。

Relations between models 模型之间的关系

Looking again at our module design, we have these relations:

再来看下我们的模块设计,我们

  • Each task has a stage – that’s a many to one relation, also known as a foreign key. The inverse relation is a one to many, meaning that each stage can have many tasks.
  • Each task can have many tags – that’s a many to many relation. The inverse relation, of course, is also a many to many, since each tag can also have many tasks.

Let’s add the corresponding relation fields to the to-do tasks in our todo_ui/todo_model.py file:

让我们在todo_ui/todo_model.py文件中对to-do tasks添加对应的关联字段:

  1. class TodoTask(models.Model):
  2. _inherit = 'todo.task'
  3. stage_id = fields.Many2one('todo.task.stage', 'Stage')
  4. tag_ids = fields.Many2many('todo.task.tag', string='Tags')

The preceding code shows the basic syntax for these fields, setting the related model and the eld’s title string. The convention for relational field names is to append _id or _ids to the field names, for to one and to many relations, respectively.

上面的代码展示了这些字段的基本语法,

As an exercise, you may try to also add on the related models, the corresponding inverse relations:

作为练习,你可以试着

  • The inverse of the Many2one relation is a One2many field on stages: each stage can have many tasks. We should add this field to the Stage class.
  • The inverse of the Many2many relation is also a Many2many field on tags: each tag can also be used on many tasks.
    Let’s have a closer look at relational field definitions.

Many to one relations 多对一关系

Many2one accepts two positional arguments: the related model (corresponding to the comodel keyword argument) and the title string. It creates a field in the database table with a foreign key to the related table.

Many2one 接受两个位置参数:关联模型(对应到联合模型的关键字参数)以及title字符串。它使用一个关联到表的外键创建了一数据库表中的字段。

Some additional named arguments are also available to use with this type of field:

一些额外的命名参数也可以

  • ondelete defines what happens when the related record is deleted. Its default is set null, meaning it is set to an empty value if the related record is deleted. Other possible values are restrict, raising an error preventing the deletion, and cascade also deleting this record.

  • ondelete 定义了当关联记录被删除时所执行的动作。

  • context and domain are meaningful for the web client views. They can be set on the model to be used by default on any view where the field is used. They will be better explained in the Chapter 6, Views - Designing the User Interface.

  • 上下文和域对web客户端视图是很有意义的。它们可以设置模型上以便用于

  • auto_join=True allows the ORM to use SQL joins when doing searches using this relation. By default this is False to be able to enforce security rules. If joins are used, the security rules will be bypassed, and the user could have access to related records the security rules wouldn’t allow, but the SQL queries will be more efficient and run faster.

  • auto_join=True 允许ORM在使用这个关系进行搜索

Many to many relations 多对多关系

The Many2many minimal form accepts one argument for the related model, and it is recommended to also provide the string argument with the field title.

Many2many最小化形式接收一个关联模型的字段,

At the database level, this does not add any column to the existing tables. Instead, it automatically creates a new relation table with only two ID fields with the foreign keys to the related tables. The relation table name and the eld names are automatically generated. The relation table name is the two table names joined with an underscore with _rel appended to it.

在数据库层面,这样做不会对现有的模型添加任何列。相反,它自动的创建一个新的

These defaults can be manually overridden. One way to do it is to use the longer form for the field definition:

这些默认值可以被手动地重写。

  1. # TodoTask class: Task <-> Tag relation (long form):
  2. tag_ids = fields.Many2many(
  3. 'todo.task.tag', # related model
  4. 'todo_task_tag_rel', # relation table name
  5. 'task_id', # field for "this" record
  6. 'tag_id', # field for "other" record
  7. string='Tasks')

Note that the additional arguments are optional. We could just set the name for the relation table and let the field names use the automatic defaults.

注意额外地参数是可选的。我们可以只设置关联表的名称,

If you prefer, you may use the long form using keyword arguments instead:

要是你喜欢的话,你可以使用下面的

  1. # TodoTask class: Task <-> Tag relation (long form):
  2. tag_ids = fields.Many2many(
  3. comodel_name='todo.task.tag', # related model
  4. relation='todo_task_tag_rel', # relation table name
  5. column1='task_id', # field for "this" record
  6. column2='tag_id', # field for "other" record
  7. string='Tasks')

Like many to one fields, many to many fields also support the domain and context keyword attributes.

像多对一字段,多对多字段也支持域和上下文关键字属性。

On some rare occasions we may have to use these long forms to override the automatic defaults, in particular, when the related models have long names or when we need a second many to many relation between the same models.

在某些很少的情况下,我们必须使用这些

Tips

PostgreSQL table names have a limit of 63 characters, and this can be a problem if the automatically generated relation table name exceeds that limit. That is a case where we should manually set the relational table name using the relation attribute.

提示

PostgreSQL表名称存在63个字符的限制,这对于自动生成的关联表名称超过限制时是个问题。这种情况下,我们应该使用关联属性手动地设置关联表的名称。

The inverse of the Many2many relation is also a Many2many field. If we also add a Many2many field to the tags, Odoo infers that this many to many relation is the inverse of the one in the task model.

Many2many关系的反向关系也可以是一个Many2many字段。如果我们也可以对tags添加Many2many字段,Odoo引用这个多对多关系

The inverse relation between tasks and tags can be implemented like this:

task和tag之间的反向关系可以像下面代码这样来实现:

  1. #class Tag(models.Model):
  2. # _name = 'todo.task.tag'
  3. #Tag class relation to Tasks:
  4. task_ids = fields.Many2many('todo.task', # related model
  5. string='Tasks')

One to many inverse relations 一对多的反向关系

The inverse of a Many2one can be added to the other end of the relation. This has no impact on the actual database structure, but allows us easily browse from the “one” side the “many” side records. A typical use case is the relation between a document header and its lines.

Many2one字段的反向可以被添加关联的尾部。这对于实际的数据库结构没有影响,而且允许我们更容易的从“一”端到“多”端的记录。典型的用法是文档头部和尾部之前的关系。

On our example, with a One2many inverse relation on stages, we could easily list all the tasks in that stage. To add this inverse relation to stages, add the code shown here:

在我们的例子中,对stage使用One2many反向关系,我们可以轻松地列出这个stage中的所有任务。为了将这个反向关系添加到stage,请添加如下代码:

  1. # class Stage(models.Model):
  2. # _name = 'todo.task.stage'
  3. # Stage class relation with Tasks:
  4. tasks = fields.One2many(
  5. 'todo.task', # related model
  6. 'stage_id', # field for "this" on related model
  7. 'Tasks in this stage')

The One2many accepts three positional arguments: the related model, the field name in that model referring this record, and the title string. The two first positional arguments correspond to the comodel_name and inverse_name keyword arguments.

One2many接受三个位置参数:关联的模型,这个模型中引用该条记录的字段名称,以及标题字符串。前面两个位置参数对应的是comodel_name和inverse_name关键字参数。

The additional keyword parameters available are the same as for many to one: context, domain, ondelete (here acting on the “many” side of the relation), and auto_join.

其它的关键字参数同样也可以应用到多对一字段:context, domain, ondelete(这里它扮演的是“多”这一边的关系),以及auto_join。

Hierarchical relations 层次关系

Parent-child relations can be represented using a Many2one relation to the same model, to let each record reference its parent. And the inverse One2many makes it easy for a parent to keep track of its children.

父子关系可以使用 Many2one关系在相同的模型中表现出来,以便让每条记录引用自身的父记录。

Odoo also provides improved support for these hierarchic data structures: faster browsing through tree siblings, and simpler search with the additional child_of operator in domain expressions.

Odoo还提供了对这些分层数据结构的改进:

To enable these features we need to set the _parent_store flag attribute and add the helper elds: parent_left and parent_right. Mind that this additional operation comes at storage and execution time penalties, so it’s best used when you expect to read more frequently than write, such as a the case of a category tree.

为了启用这些功能,我们需要设置旗帜属性_parent_store,并添加辅助字段:parent_left和parent_right。注意这个额外的操作带来的是存储与执行时间上的不利,所以当你希望读取更频繁于写入时最好使用它,比如在目录树的例子中。

Revisiting the tags model defined in the todo_ui/todo_model.py file, we should now edit it to look like this:

重新反问定义在todo_ui/todo_model.py文件中的tag模型,我们应该编辑这个文件让它看起来是这个样子:

  1. class Tags(models.Model):
  2. _name = 'todo.task.tag'
  3. _parent_store = True
  4. # _parent_name = 'parent_id'
  5. name = fields.Char('Name')
  6. parent_id = fields.Many2one(
  7. 'todo.task.tag', 'Parent Tag', ondelete='restrict')
  8. parent_left = fields.Integer('Parent Left', index=True)
  9. parent_right = fields.Integer('Parent Right', index=True)

Here, we have a basic model, with a parent_id field to reference the parent record, and the additional _parent_store attribute to add hierarchic search support. When doing this, the parent_left and parent_right fields also have to be added.

这里,我们编写了一个基础模型,它拥有一个引用到父记录的parent_id字段,以及额外的为了添加层次搜索支持的_parent_store 属性。在你这样做的时候,parent_leftparent_right 字段也必须添加进去。

The field referring to the parent is expected to be named parent_id. But any other field name can be used by declaring it with the _parent_name attribute.

应用到父的字段期望使用的名称是parent_id。但是其它的任意字段名称也能够和_parent_name属性放到一起声明。

Also, it is often convenient to add a field with the direct children of the record:

而且,对一个字段直接的添加记录的子记录也是很方便的:

  1. child_ids = fields.One2many('todo.task.tag', 'parent_id', 'Child Tags')

Referencing fields using dynamic relations 使用动态关系应用字段

So far, the relation fields we’ve seen can only reference one model. The Reference field type does not have this limitation and supports dynamic relations: the same field is able to refer to more than one model.

到目前为止,我们所见到的关系字段只能够引用一个模型。Reference的字段去不存在这个限制,而且还支持动态属性:相同的字段能够引用不止一个模型。

We can use it to add a To-do Task field, Refers to, that can either refer to a User or a Partner:

我们可以使用它来添加一个To-do Task的字段,Refers to,它能够引用User或者Partner中的任何一个:

  1. # class TodoTask(models.Model):
  2. refers_to = fields.Reference([('res.user', 'User'), ('res.partner', 'Partner')], 'Refers to')

You can see that the field definition is similar to a Selection field, but here the selection list holds the models that can be used. On the user interface, the user will rst pick a model from the list, and then pick a record from that model.

你能够给发现字段的定义类似于Selection字段,但是这里的下拉列表拥有可被使用的模型。

This can be taken to another level of flexibility: a Referencable Models con guration table exists to con gure the models that can be used in Reference elds. It is available in the Settings | Technical | Database Structure menu. When creating such a eld we can set it to use any model registered there, with the help of the referencable_models() function in the openerp.addons.res.res_request module. In Odoo version 8, it is still using the old-style API, so we need to wrap it to use with the new API:

  1. from openerp.addons.base.res import res_request
  2. def referencable_models(self):
  3. return res_request.referencable_models(self, self.env.cr, self.env.uid, context=self.env.context)

Using the preceding code, the revisited version of the Refers to field would look like this:

  1. # class TodoTask(models.Model):
  2. refers_to = fields.Reference(referencable_models, 'Refers to')

Computed fields 计算字段

Fields can have values calculated by a function, instead of simply reading a database stored value. A computed eld is declared just like a regular eld, but has an additional argument compute with the name of the function used to calculate it.

In most cases computed elds involve writing some business logic, so we will develop this topic more in Chapter 7, ORM Application Logic - Supporting Business Processes. We can still explain them here, but keeping the business logic side as simple as possible.

Let’s work on an example: stages have a fold eld. We will add to tasks a computed eld with the Folded? ag for the corresponding stage.

We should edit the TodoTask model in the todo_ui/todo_model.py le to add the following:

  1. class TodoTask(models.Model):
  2. stage_fold = fields.Boolean('Stage Folded?', compute='_compute_stage_fold')
  3. @api.one
  4. @api.depends('stage_id.fold')
  5. def _compute_stage_fold(self):
  6. self.stage_fold = self.stage_id.fold

The preceding code adds a new stage_fold field and the _compute_stage_fold method used to compute it. The function name was passed as a string, but it’s also allowed to pass it as a callable reference (the function identi er with no quotes).

之前的代码添加了一个新的stage_fold字段,

Since we are using the @api.one decorator, self will represent a single record. If we used @api.multi instead, it would represent a recordset and our code would need to handle the iteration over each record.

因为我们使用的是@api.one装饰器,

The @api.depends is necessary if the computation uses other fields: it tells the server when to recompute stored or cached values. It accepts one or more field names as arguments and dot-notation can be used to follow eld relations.

@api.depends

The computation function is expected to assign a value to the field or fields to compute. If it doesn’t, it will error. Since self is a record object, our computation is simply to get the Folded? field using self.stage_id.fold. The result is achieved by assigning that value (writing it) to the computed field, self.stage_fold.

We won’t be working yet on the views for this module, but you can make a quick edit on the task form to confirm if the computed field is working as expected: using the Developer Menu pick the Edit View option and add the eld directly in the form XML. Don’t worry: it will be replaced by the clean module view on the next upgrade.

我们不会

Search and write on computed fields 对计算字段进行查找和更新

The computed field we just created can be read, but it can’t be searched or written. This can be enabled by providing specialized functions for that. Along with the compute function, we can also set a search function, implementing the search logic, and the inverse function, implementing the write logic.

In order to do this, our computed field declaration becomes like this:

我们实现这个目标,我们想下面这样计算字段声明:

  1. # class TodoTask(models.Model):
  2. stage_fold = fields.Boolean(string='Stage Folded?',
  3. compute='_compute_stage_fold',
  4. # store=False) # the default
  5. search='_search_stage_fold',
  6. inverse='_write_stage_fold')

The supporting functions are:

支持的函数如下:

  1. def _search_stage_fold(self, operator, value):
  2. return [('stage_id.fold', operator, value)]
  3. def _write_stage_fold(self):
  4. self.stage_id.fold = self.stage_fold

The search function is called whenever a (field, operator, value) condition on this eld is found in a search domain expression. It receives the operator and value for the search and is expected to translate the original search element into an alternative domain search expression.

搜索函数被调用

The inverse function performs the reverse logic of the calculation, to nd the value to write on the source elds. In our example, it’s just writing on stage_id.fold.

Storing computed fields 存储计算字段

Computed field’s values can also be stored on the database, by setting store to True on their definition. They will be recomputed when any of their dependencies change. Since the values are now stored, they can be searched just like regular elds, so a search function is not needed.

计算字段的值也可以被存储在数据库上,通过设置store为True

The computed field we implemented in the previous section is a special case that can be automatically handled by Odoo. The same effect can be achieved using Related fields. They make available, directly on a model, elds that belong to a related model, accessible using a dot-notation chain. This makes them usable in situations where dot-notation can’t be used, such as UI forms.

计算我们在前面小节中实现的字段是个特殊情形,它可以由Odoo自动的来处理。

To create a related eld, we declare a eld of the needed type, just like with regular computed elds, and instead of compute, use the related attribute indicating the dot-notation eld chain to reach the desired eld.

To-do tasks are organized in customizable stages and these is turn map into basic states. We will make them available on tasks, and will use this for some client-side logic in the next chapter.

Similarly to stage_fold, we will add a computed eld on the task model, but now using the simpler Related eld:

  1. # class TodoTask(models.Model):
  2. stage_state = fields.Selection(
  3. related='stage_id.state',
  4. string='Stage State')

Behind the scenes, Related elds are just computed elds that conveniently implement search and inverse. This means that we can search and write on them out of the box, without having to write any additional code.

Model constraints 模型限制

To enforce data integrity, models also support two types of constraints: SQL and Python.

要强制启用数据的完整性,模型也可以支持两个类型的约束:SQL和Python。

SQL constraints are added to the table definition in the database and implemented by PostgreSQL. They are defined using the class attribute _sql_constraints. It is a list of tuples with the constraint identifier name, the SQL for the constraint, and the error message to use.

SQL约束被添加到了数据库中定义的,由PostgreSQL实现的表。它们使用类属性_sql_constraints来定义。

A common use case is to add unique constraints to models. Suppose we didn’t want to allow the same user to have two active tasks with the same title:

  1. # class TodoTask(models.Model):
  2. _sql_constraints = [
  3. ('todo_task_name_uniq',
  4. 'UNIQUE (name, user_id, active)',
  5. 'Task title must be unique!')]

Since we are using the user_id field added by the todouser module, this dependency should be added to the depends key of the `_openerp.py` manifest file.

因为我们

Python constraints can use a piece of arbitrary code to check conditions. The checking function needs to be decorated with @api.constrains indicating the list of elds involved in the check. The validation is triggered when any of them is modi ed, and will raise an exception if the condition fails:

Python约束

  1. from openerp.exceptions import ValidationError
  2. # class TodoTask(models.Model):
  3. @api.one
  4. @api.constrains('name')
  5. def _check_name_size(self):
  6. if len(self.name) < 5:
  7. raise ValidationError('Must have 5 chars!')

The preceding example prevents saving task titles with less than 5 characters.

上面的例子阻止了少于5个字符的任务名称的保存。

Summary 总结

We went through a thorough explanation of models and elds, using them to extend the To-do app with tags and stages on tasks. You learned how to de ne relations between models, including hierarchical parent/child relations. Finally, we saw simple examples of computed elds and constraints using Python code.

我们通过

In the next chapter, we will work on the user interface for these back-end model features, making them available in the views used to interact with the application.

在下一章,我们