模型之间的关系
来自模型的记录可能与来自另一模型的记录相关。例如,一个销售订单记录和一个包含客户数据的客户记录相关;同时也和销售订单线记录相关。
练习
创建会话模型
对于模块Open Academy,我们考虑一个会话模型:会话是在给定时间为给定听众授课的课程。
为会话创建模型。一个会话有一个名字,一个开始日期,一个持续时间和一些座位。 添加一个动作和菜单项来显示它们。通过菜单项使新模型可见。
- 在openacademy/models/models.py中创建类Session
- 在openacademy/view/openacademy.xml中添加对会话对象的访问
openacademy/models.py
name = fields.Char(string="Title", required=True)
description = fields.Text()
class Session(models.Model):
_name = 'openacademy.session'
name = fields.Char(required=True)
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
openacademy/views/openacademy.xml
<!-- Full id location:
action="openacademy.course_list_action"
It is not required when it is the same module -->
<!-- session form view -->
<record model="ir.ui.view" id="session_form_view">
<field name="name">session.form</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<form string="Session Form">
<sheet>
<group>
<field name="name"/>
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="session_menu" name="Sessions"
parent="openacademy_menu"
action="session_list_action"/>
</data>
</odoo>
注
digits=(6, 2)
指定浮点数的精度:6是数字的总数,而2是逗号后的位数。注意逗号之前的数字的位数最大为4
关系字段
关系字段链接记录,即相同模型(层次结构)或不同模型之间的记录。
关系字段类型为:
Many2one(other_model, ondelete='set null')
与其他对象的简单链接:
print foo.other_id.name
请参阅
One2many(other_model, related_field)
一个虚拟关系,一个Many2one
的逆关系。One2many
表现为一个记录容器,访问它导致一组(可能是空的)记录集合:
for other in foo.other_ids:
print other.name
危险
因为One2many
是一个虚拟关系,所以必须在other_model中有一个Many2one
字段,它的名字必须是related_field
双向多重关系,任何一方的记录都可以与另一方的任何数量的记录相关。作为记录容器,访问它也会导致一个可能的空记录集:
for other in foo.other_ids:
print other.name
练习
多对一关系
使用many2one,修改Course 和Session模型,以反映它们与其他模型的关系:
- course有一个responsible用户;那个字段的值是内置模型res.users的记录
- session有一个instructor;该字段的值是一个内置的模型res.partner记录
- session和course是相关的;其字段的值是模型openacademy.course的记录,也是必须的
- 适配视图
- 将相关的Many2one字段添加到模型中,以及
- 在视图中添加它们
openacademy/models.py
name = fields.Char(string="Title", required=True)
description = fields.Text()
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
class Session(models.Model):
_name = 'openacademy.session'
start_date = fields.Date()
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
openacademy/views/openacademy.xml
<sheet>
<group>
<field name="name"/>
<field name="responsible_id"/>
</group>
<notebook>
<page string="Description">
</field>
</record>
<!-- override the automatically generated list view for courses -->
<record model="ir.ui.view" id="course_tree_view">
<field name="name">course.tree</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<tree string="Course Tree">
<field name="name"/>
<field name="responsible_id"/>
</tree>
</field>
</record>
<!-- window action -->
<!--
The following tag is an action definition for a "window action",
<form string="Session Form">
<sheet>
<group>
<group string="General">
<field name="course_id"/>
<field name="name"/>
<field name="instructor_id"/>
</group>
<group string="Schedule">
<field name="start_date"/>
<field name="duration"/>
<field name="seats"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<!-- session tree/list view -->
<record model="ir.ui.view" id="session_tree_view">
<field name="name">session.tree</field>
<field name="model">openacademy.session</field>
<field name="arch" type="xml">
<tree string="Session Tree">
<field name="name"/>
<field name="course_id"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="session_list_action">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
练习
逆一对多关系
使用逆关系字段one2many,修改模型以反映courses 与sessions之间的关系
- 修改
Course
类,并且 - 在course 表单视图中添加字段
openacademy/models.py
responsible_id = fields.Many2one('res.users',
ondelete='set null', string="Responsible", index=True)
session_ids = fields.One2many(
'openacademy.session', 'course_id', string="Sessions")
class Session(models.Model):
openacademy/views/openacademy.xml
<page string="Description">
<field name="description"/>
</page>
<page string="Sessions">
<field name="session_ids">
<tree string="Registered sessions">
<field name="name"/>
<field name="instructor_id"/>
</tree>
</field>
</page>
</notebook>
</sheet>
练习
多重多对多关系
使用关系字段many2many,修改Session模型以将每个session 与一组attendees联系起来。Attendees 将由partner 记录表示,因此我们将关联内置的模型res.partner。相应地调整视图。
- 修改
Session
类,并且 - 在表单视图中添加字段
openacademy/models.py
instructor_id = fields.Many2one('res.partner', string="Instructor")
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
openacademy/views/openacademy.xml
<field name="seats"/>
</group>
</group>
<label for="attendee_ids"/>
<field name="attendee_ids"/>
</sheet>
</form>
</field>
继承
模型继承
Odoo提供了两种继承机制,以模块化方式扩展现有模型。
第一个继承机制允许一个模块修改另一个模块中定义一个模型的行为:
- 将字段添加到模型中
- 重写模型上字段的定义
- 向模型添加约束
- 将方法添加到模型中
- 重写模型上的现有方法
第二继承机制(委托)允许将模型的每个记录链接到父模型中的记录,并提供对父记录的字段的透明访问。
请参阅
视图继承
代替修改现有的视图(通过重写它们),Odoo提供的视图继承让子视图“扩展”的视图应用在顶部的根视图,并可以从父视图中添加或删除内容。
扩展视图使用inherit_id
字段引用它的父类,而不是单个视图,其arch
字段由任意数量的xpath
元素组成,它们选择和改变父视图的内容:
<!-- improved idea categories list -->
<record id="idea_category_list2" model="ir.ui.view">
<field name="name">id.category.list2</field>
<field name="model">idea.category</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">
<!-- find field description and add the field
idea_ids after it -->
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
</record>
expr
在父视图中XPath表达式选择单个元素。如果不匹配元素或多于一个,则会引发错误
position
应用于匹配元素的操作:
inside
在匹配元素的末尾追加xpath的body
replace
替换使用xpath的body的匹配元素,替换任何 $0
节点发生在使用原始元素的新的body中
before
在匹配元素之前插入xpat的body作为兄弟
after
匹配元素之后插入xpat的body作为兄弟
attributes
使用在xpath的body中的特殊的attribute
元素改变匹配元素的属性
提示
当匹配一个元素,position
属性可以直接设置在元素上以便被发现。下面的两个继承将给出相同的结果
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
练习
更改现有内容
- 使用模型继承,修改现有的Partner 模型以添加
instructor
布尔字段,以及对应于session-partner关系的many2many 字段 - 使用视图继承,在partner表单视图中显示此字段
注
这是引入开发者模式来检查视图、找到其外部ID和放置新字段的位置的机会
- 创建文件
openacademy/models/partner.py
并在__init__.py
中导入 - 创建文件
openacademy/views/partner.xml
并在__manifest__.py中添加
openacademy/__init__.py
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import partner
openacademy/__manifest__.py
# 'security/ir.model.access.csv',
'templates.xml',
'views/openacademy.xml',
'views/partner.xml',
],
# only loaded in demonstration mode
'demo': [
openacademy/partner.py
# -*- coding: utf-8 -*-
from odoo import fields, models
class Partner(models.Model):
_inherit = 'res.partner'
# Add a new column to the res.partner model, by default partners are not
# instructors
instructor = fields.Boolean("Instructor", default=False)
session_ids = fields.Many2many('openacademy.session',
string="Attended Sessions", readonly=True)
openacademy/views/partner.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<!-- Add instructor field to existing view -->
<record model="ir.ui.view" id="partner_instructor_form_view">
<field name="name">partner.instructor</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Sessions">
<group>
<field name="instructor"/>
<field name="session_ids"/>
</group>
</page>
</notebook>
</field>
</record>
<record model="ir.actions.act_window" id="contact_list_action">
<field name="name">Contacts</field>
<field name="res_model">res.partner</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="configuration_menu" name="Configuration"
parent="main_openacademy_menu"/>
<menuitem id="contact_menu" name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
</data>
</odoo>
域
在Odoo中,域是对记录条件进行编码的值。一个域是一个列表,用于选择一个模型的记录子集的标准。每个标准是一个三元组,有一个字段名、一个运算符和一个值。
例如,当使用Product 模型下列域选择所有是服务且单价超过1000的产品:
[('product_type', '=', 'service'), ('unit_price', '>', 1000)]
默认情况下,标准使用隐式的AND联合。逻辑运算符 &
(AND), |
(OR) 和!
(NOT)可用来显示的联合标准。它们在前缀位置中使用(操作符在其参数之前插入而不是在它们之间)。例如,选择合适的产品“其是服务或有单价不是在1000和2000之间”:
['|',
('product_type', '=', 'service'),
'!', '&',
('unit_price', '>=', 1000),
('unit_price', '<', 2000)]
当尝试在客户端界面中选择记录时,可以将domain
参数添加到关系字段中,以限制关系的有效记录
练习
关系字段上的域
当为Session选择instructor时,仅instructors (partners 的instructor
设置为 True
)应该可见。
openacademy/models.py
duration = fields.Float(digits=(6, 2), help="Duration in days")
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=[('instructor', '=', True)])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
注
声明为文字列表的域被评估为服务器端,不能引用右侧的动态值,声明为字符串的域被评估为客户端,并允许右侧的字段名
练习
更多的复杂域
创建新的partner categories Teacher / Level 1 and Teacher / Level 2. 对于一个session的instructor可以是 instructor或teacher(任何级别)
- 修改Session 模型的域
修改
openacademy/view/partner.xml
以访问Partner categories:
openacademy/models.py
seats = fields.Integer(string="Number of seats")
instructor_id = fields.Many2one('res.partner', string="Instructor",
domain=['|', ('instructor', '=', True),
('category_id.name', 'ilike', "Teacher")])
course_id = fields.Many2one('openacademy.course',
ondelete='cascade', string="Course", required=True)
attendee_ids = fields.Many2many('res.partner', string="Attendees")
openacademy/views/partner.xml
<menuitem id="contact_menu" name="Contacts"
parent="configuration_menu"
action="contact_list_action"/>
<record model="ir.actions.act_window" id="contact_cat_list_action">
<field name="name">Contact Tags</field>
<field name="res_model">res.partner.category</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem id="contact_cat_menu" name="Contact Tags"
parent="configuration_menu"
action="contact_cat_list_action"/>
<record model="res.partner.category" id="teacher1">
<field name="name">Teacher / Level 1</field>
</record>
<record model="res.partner.category" id="teacher2">
<field name="name">Teacher / Level 2</field>
</record>
</data>
</odoo>
计算字段和默认值
到目前为止,字段已经直接存储在数据库中并直接从数据库中检索。也可以计算字段。在这种情况下,字段的值不是从数据库中检索的,而是通过调用模型的方法即时计算的。
若要创建计算字段,请创建字段并将其属性compute
设置为方法的名称。计算方法应该简单地设置self中每个记录的字段值。
危险
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
练习
计算字段
- 将所占席位的百分比添加到Session模型中
- 在树和表单视图中显示字段
- 将字段显示为进度
- 向Session添加计算字段
- 在Session视图中显示字段 view:
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文字值(布尔、整数、浮点、字符串),或者是一个记录集并返回一个值的函数:
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
是当前用户的数据库idself.env.user
是当前用户的记录self.env.context
或self._context
是上下文字典self.env.ref(xml_id)
返回对应于XML id的记录self.env[model_name]
返回给定模型的实例
练习
活动对象-默认值
- 定义start_date字段默认值为今天(参见
Date
) - 在Session类中添加字段
active
, 并且默认情况下设置sessions为活动的
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,并且当任何其他字段被修改时,您希望更新表单上的价格。为了实现这一点,定义一种方法,其中self
表示表单视图中的记录,并用onchange()
来装饰它,以指定它必须触发哪个字段。你在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
行为是内置的,如可以通过播放Session 表单看到的:更改座位或参与者的数量,自动更新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",
},
}
上一篇:Odoo10教程---模块化一:新建一个模块及基本视图
下一篇:Odoo10教程---模块化三:模型约束,高级视图,工作流,安全性,向导,国际化和报表等
代码下载地址:https://download.csdn.net/download/mzl87/10389201
ps:有翻译不当之处,欢迎留言指正。
原文地址:https://www.odoo.com/documentation/10.0/howtos/backend.html