全书完整目录请见:Odoo 14开发者指南(Cookbook)第四版
本章中的各小节会对已有的那个插件模型做一些小的新增。在上一单中,我们在Odoo实例中注册了自己的插件模块。本章中,我们将深入到模块的数据库端。会添加一个新模型(数据表)、一些新的字符和约束。我们还会研究Odoo中继承的使用。使用的是第三章 创建Odoo插件模块中所创建的模块。
本章中讲解如下小节:
- 定义模型表现及排序
- 向模型添加数据字段
- 使用可配置精度的浮点字段
- 向模型添加货币字段
- 向模型添加关联字段
- 向模型添加等级
- 向模型添加约束验证
- 向模型添加计算字段
- 暴露存储在其它模型中的关联字段
- 使用引用字段添加动态关联
- 使用继承向模型添加功能
- 使用继承拷贝模型定义
- 使用代理继承将功能拷贝至另一个模型
- 使用抽象模型实现可复用模型功能
技术准备
要按照本章中的示例进行操作,你应该要有第三章 创建Odoo插件模块中所创建的模块并且该模型应可用。
本章中使用的代码可以在GitHub仓库中进行下载,地址为https://github.com/alanhou/odoo14-cookbook/tree/main/Chapter04。
译者注:本文中对多处模型修改相应的视图字段修改未作表述,请参见 GitHub代码views/library_book.xml文字
定义模型表示及排序
模型中有结构性属性来定义它们的行为。这些以下划线作为前缀。最重要的属性是_name,因为这定义了内部全局标识符。Odoo内部通过这一_name属性来创建数据表。例如,如果你使用_name=”library.book”,那么Odoo ORM会在数据库中创建一张library_book数据表。这也是为什么_name必须在Odoo系统中要保持唯一。
在模型中可以使用另外两个属性:
- _rec_name 是一个设置用于展示或记录标题的字段
- 另一个是_order, 用于设置记录的展现的顺序。
准备工作
这一节中我们假定你已经有一个my_library模块的实例,如第三章 创建Odoo插件模块中所描述。
如何实现…
my_library实例应包含一个名为models/library_book.py的Python文件,它定义了一个基础模型。我们将编辑该文件来在_name之后添加一个新的类级属性:
- 加入如下代码来添加一个用户友好的模型标题:
1_description = 'Library Book'
译者注:为避免读者对命令行中的 WARNING 产生困扰,我在上一章的示例代码中已添加该属性 - 首先对记录进行排序(按时间最近排序,然后按标题排序),添加如下代码:
1_order = 'date_release desc, name' - 添加如下代码来使用short_name字段作为记录的表示:
12_rec_name = 'short_name'short_name = fields.Char('Short Title', required=True) - 在表单视图中添加short_name字段,这样会在该视图中显示这一新字段:
1<field name="short_name"/>
完成如上操作之后,我们library_book.py文件应该是下面这样:
1 2 3 4 5 6 7 8 9 10 |
from odoo import models, fields class LibraryBook(models.Model): _name = 'library.book' _description = 'Library Book' _order = 'date_release desc, name' _rec_name = 'short_name' name = fields.Char('Title', required=True) short_name = fields.Char('Short Title', required=True) date_release = fields.Date('Release Date') author_ids = fields.Many2many('res.partner', string='Authors') |
你的library_book.xml 文件中的<form>视图应该是下面这样的:
1 2 3 4 5 6 7 8 9 10 11 12 |
<form> <group> <group> <field name="name"/> <field name="author_ids" widget="many2many_tags"/> </group> <group> <field name="short_name"/> <field name="date_release"/> </group> </group> </form> |
我们应升级模块在让这些修改在Odoo中生效。升级模块,可以打开Apps菜单,搜索my_library模块,然后通过下拉菜单更新模块,如下图所示:
图4.1 – 更新模块的选项
我们也可以通过在命令行中使用-u my_library来完成模块的升级。
运行原理…
第一步为定义的模型添加了对用户更友好的标题。这并非强制的,但可以为一些插件所用。例如,它可在创建记录时用于mail插件模块中追踪功能的通知文本。更多详情,请参见第二十三章 在Odoo中管理Email。如果在模型中不使用_description,Odoo会在日志中输出一条警告。
默认Odoo使用内部id值(自动生成的主键)进行排序。但是,可对其进行修改来使用我们自己选择的字段排序,做法是提供一个包含字段名逗号分隔列表字符串的_order属性。字段名后可接desc关键字来进行降序排序。
仅能使用存储在数据库中的字段。未存储的计算字段无法用于记录排序。
📝重要:仅可使用数据库中存储的字段。未存储的计算字段无法用于记录排序
_order字符串的语法类似于SQL中的ORDER BY语句,但进行了简化。例如,不允许NULLS FIRST这样的特殊语句。
模型记录在其它记录中引用时使用了一种表现形式。例如,带有值1的user_id表示管理员用户。在表单视图中显示时,Odoo会显示用户名,而非数据库ID。简言之,_rec_name属性是在Odoo图形界面中用于展示该记录的记录显示名称。默认使用的是name字段。本例中,library.book模型有一个name字段,因此默认Odoo使用它作为显示名称。我们在第3步中对这一行为进行了修改,使用short_name作为_rec_name。之后,library.book模型的显示名称由name变为了short_name,Odoo GUI会使用short_name的值来展示记录。
📝警告:如果模型中没有name字段且未指定_rec_name,显示名称将是模型名称和记录ID的组合,如(library.book, 1)。
因为我们在模型中新增了short_name字段,Odoo ORM会在数据表中新增一列,但该字段不会在视图中显示。要进行显示,我们需要在表单视图中添加该字段。在第4步中,我们在表单视图中添加了short_name。
扩展知识…
记录表示可采用一个魔法计算字段display_name,从版本8.0起自动添加到了所有模型中。它的值由name_get() 模型方法生成,在Odoo此前的版本中已存在这一方法。
name_get()的默认实现使用_rec_name属性来查找存放数据的字段,使用它生成显示名称。如果想要自己实现显示名称,可以重载name_get()中的逻辑来生成一个自定义显示名称。该方法应返回一个包含两个元素的元组列表:记录ID和记录的Unicode字符串表示。
例如,在表示中包含标题和发行日期,类似Moby Dick (1851-10-18),我们可以进行如下定义:
看下如下示例。它会在记录名称中添加一个发布日期:
1 2 3 4 5 6 |
def name_get(self): result = [] for record in self: rec_name = "%s (%s)" % (record.name, record.date_release) result.append((record.id, rec_name)) return result |
添加如下代码后,就会更新display_name记录。假高有一条名为Odoo Cookbook、发行日期为19-04-2019,是的记录,那么上述的name_get()方法会生成一个Odoo Cookbook (19-04-2019).这样的名称。
向模型添加数据字段
模型用于存储数据,数据按字段进行组织。这里,我们将学习到可存储在字段中的不同数据类型,以及如何在模型中进行添加。
准备工作
学习这一节假定读者已经有一个如第三章 创建Odoo插件模块中所述的带有my_library插件模块的实例准备就绪。
如何实现…
my_library插件模块应该已经有定义了基本模型的models/library_book.py文件,我们将编辑该文件来新增字段:
- 使用最小化语法来向图书模型添加字段:
1234567891011121314151617181920from odoo import models, fieldsclass LibraryBook(models.Model):# ...short_name = fields.Char('Short Title')notes = fields.Text('Internal Notes')state = fields.Selection([('draft', 'Not Available'),('available', 'Available'),('lost', 'Lost')],'State')description = fields.Html('Description')cover = fields.Binary('Book Cover')out_of_print = fields.Boolean('Out of Print?')date_release = fields.Date('Release Date')date_updated = fields.Datetime('Last Updated')pages = fields.Integer('Number of Pages')reader_rating = fields.Float('Reader Average Rating',digits=(14, 4), # Optional precision (total, decimals),) - 我们已向模型新增了字段。仍需在表单视图中添加这些字段来在用户界面中反映出这些修改。参见如下在表单视图中增加字段的代码:
123456789101112131415161718192021<form><group><group><field name="name"/><field name="author_ids" widget="many2many_tags"/><field name="state"/><field name="pages"/><field name="notes"/></group><group><field name="short_name"/><field name="date_release"/><field name="date_updated"/><field name="cover" widget="image" class="oe_avatar"/><field name="reader_rating"/></group></group><group><field name="description"/></group></form>
升级模块会让Odoo模型中这些修改生效。
查看如下这些不同字段的示例。这里我们对字段使用了不同类型的属性。这会让读者对字段声明拥有更好的概念:
1 2 3 4 5 6 7 8 9 10 11 |
short_name = fields.Char('Short Title',translate=True, index=True) state = fields.Selection( [('draft', 'Not Available'), ('available', 'Available'), ('lost', 'Lost')], 'State', default="draft") description = fields.Html('Description', sanitize=True, strip_style=False) pages = fields.Integer('Number of Pages', groups='base.group_user', states={'lost': [('readonly', True)]}, help='Total book page count', company_dependent=False) |
运行原理…
通过在Python类中的定义属性向模型添加字段。可以使用的非关联字段类型如下:
- Char用于字符串值。
- Text用于多行字符串值。
- Selection用于选择列表。这是一个值和描述对的列表。所选择的值存储于数据库中,可以是字符串或整型。描述自动可翻译。
📝重要贴士:在Selection类型的字段中,可以使用整型的键,但应注意Odoo内部将0解释为未设置,不会显示存储值为0的描述。这可能会发生,所以应当铭记于心。
- Html类似于text字段,但通常用于以HTML格式存储的富文本。
- Binary字段存储二进制文件,如图像或文档。
- Boolean存储True/False 值。
- Date存储日期值。在数据库中以日期进行存储。ORM中以Python date对象的形式对其进行处理。可以使用fields.Date.today()在日期字段中将当前日期设置为默认值。Odoo 12之前的版本中ORM以字符串的形式处理日期。所使用的格式在odoo.fields.DATE_FORMAT中定义。
- Datetime用于日期时间值。在数据库中以原生UTC时间datetime进行存储。ORM中以Python datetime对象的形式对其进行处理。可以使用fields.Date.now()在日期时间字段中将当前时间设置为默认值。Odoo 12之前的版本中ORM以字符串的形式处理datetime。所使用的格式在odoo.fields.DATETIME_FORMAT中定义。
- Integer字段无需过多解释了。
- Float(浮点)字段存储数值。精度可由位数和小数位数对来定义。
- Monetary可存储某个币种的数量值。这会在本章中的向模型添加货币字段一节进行讲解。
本节的第一步中展示了添加各个字段类型的最小化语法。字段定义可像第2步中那样进行扩展来添加其它可选属性。
以下是有关所使用的属性的讲解:
- string是字段的标题,在UI视图标签中使用。它是可选项,如未设置,会通过首字母大写及将空格替换为下划线来从字段名获取标签。
- translate,在设置为True时,让字段可翻译,它可根据用户界面的语言保存不同值。
- default是默认值。也可以是一个用于计算默认值的函数,例如default=_compute_default,其中_compute_default是在定义字段前模型中所定义的一个方法。
- help是在UI提示中显示的解释性文本。
- groups让字段仅对安全组可用。它是包含安全组XML ID逗号分隔列表的一个字符串。这个话题将会在第十章 权限安全中进行讨论。
- states允许用户界面依据state字段的值来动态设置readonly, required和invisible属性值。因此,它要求存在一个state字段并在表单视图中使用(即便是隐藏的)。state属性的名称是在Odoo硬编码且无法修改的。
- copy标记在复制记录时是否拷贝字段值。对于非关联型字段和Many2one字段它的默认值是True、对One2many和计算字段它的默认值是False。
- index,在设置为True时,为该字段创建一个数据库索引,有时可供更快速搜索使用。它取代了已弃用的select=1属性。
- readonly标记让该字段在用户界面中默认仅为只读。
- required标记强制字段在用户界面中默认为必填。
📝这里所提到的各种白名单在odoo/tools/mail.py中进行定义。
- company_dependent标记让该字段按公司/租户存储不同值。它取代了已弃用的Property字段类型。
- group_operator 是一个用于按模式在组中显示结果的聚合函数。该属性的可用值有count、count_distinct、array_agg、bool_and、bool_or、max、min、avg和 sum。 整型、浮点型和monetary类型该属性的默认值是sum。
- sanitize标记用于HTML字段,提取不安全标签中的内容。使用它会对输入进行全局清理。
如果需要更细粒度的HTML清理控制,可以使用一些其它关键字,仅在启用sanitize时生效:
- sanitize_tags=True删除白名单列表以外的标签(默认项)
- sanitize_attributes=True删除白名单列表以外的标签属性
- sanitize_style=True删除白名单列表以外的样式属性
- strip_style=True删除所有样式元素
- strip_class=True删除所有class属性
最后,我们根据模型中新增的字段更新了表单视图,我们可以在任意位置以自己的方式放置<field>标签。表单视图在第九章 后端视图中会进行细致的讲解。
扩展知识…
Selection字段还接收一个函数引用来代替列表作为selection属性。这允许动态生成选项列表,你会在本章的使用引用字段添加动态关联一节中看到一个示例,其中也使用了selection属性。
Date和Datetime字段对象暴露了一些非常方便的工具方法。
Date有如下方法:
- fields.Date.to_date(string_value)将字符串解析为一个date对象。
- fields.Date.to_string(date_value)将python Date对象转化为字符串。
- fields.Date.today()以字符串格式返回当天日期。这适合用于默认值。
- fields.Date.context_today(record, timestamp)根据记录(或记录集)上下文的时区以字符串格式返回时间戳对应的日期(或者在省略时间戳时返回当天日期)。
Datetime有如下方法:
- fields.Datetime.to_datetime(string_value)将字符串解析为datetime对象。
- fields.Datetime.to_string(datetime_value)将datetime对象转化为字符串。
- fields.Datetime.now()以字符串格式返回当天日期及当前时间。适合用作默认值。
- fields.Datetime.context_timestamp(record, timestamp)将时间戳原生datetime对象按照记录上下文的时区转化为对应时区。它不适合用作默认值,但是在向外部系统发送数据等操作时可以使用。
除基本字段外,我们还有关联字段:Many2one, One2many和Many2many。这些会在本章向模型添加关联字段一节中进行讲解。
字段也可以有动态计算的值,使用compute字段属性定义计算函数。这在向模型添加计算字段一节中进行讲解。
有些字段在Odoo模型中默认添加,因此我们不应在字段中使用这些名称。这些是记录自动生成的标识符的id字段以及一些审计日志字段,如下所示:
- create_date是记录创建的时间戳
- create_uid是创建该记录的用户
- write_date是最近记录的编辑时间戳
- write_uid是最后编辑记录的用户
这些日志字段的自动创建可通过设置模型属性_log_access=False来进行禁用。
另一个可以向模型添加的特殊字段是active。它应是布尔型字段,允许用户将记录标记为非活跃(inactive)。用于对记录启用存档/取消存档功能。它的定义如下:
1 |
active = fields.Boolean('Active', default=True) |
默认只有将active设置为True的记录才可见。要获取这些记录,我们需要使用域过滤器[(‘active’, ‘=’, False)]。而如果向环境上下文添加了’active_test’: False 值,ORM则不会过滤掉非活跃记录。
📝小贴士:在有些情况下,可能无法修改上下文来获取活跃及非活跃记录。这时,可以使用[‘|’, (‘active’, ‘=’, True), (‘active’, ‘=’, False)] 域。
📝注意:[(‘active’, ‘in’ (True, False))]可能不会如你所预期那样。Odoo在域中显式地查找(‘active’, ‘=’, False)语句。它默认会限制仅搜索活跃记录。
使用可配置精度的浮点字段
在使用浮点字段时,我们可能会想让终端用户自己配置所使用的精度。本节中我们会对Library Books模型添加一个成本(Cost Price)字段,让用户可配置其小数精度。
准备工作
我们将继续使用前一节中的my_library插件模块。
如何实现…
执行如下步骤来对模型的cost_price字段应用动态小数点精度:
- 通过Settings菜单下的链接启用开发者模式(参见第一章 安装Odoo开发环境中的激活Odoo开发者工具一节)。这会启用Settings > Technical菜单。
- 访问数字精度设置。这需要打开Settings顶级菜单并选择Technical > Database Structure > Decimal Accuracy。我们应该会看到一个当前定义的设置列表。
- 添加一个新配置,设置Usage为Book Price并选择Digits精度:
图4.2 – 新建数字精度 - 要使用数字精度设置添加模型字段,编辑models/library_book.py文件并添加如下代码:
12class LibraryBook(models.Model):cost_price = fields.Float('Book Cost', digits='Book Price')📝小贴士:不论何时向模型添加新字段,都需要将它们添加到视图中以在用户界面中访问。在前例中,我们添加了cost_price。要在表单视图中看到它,需要添加<field name=”cost_price”/>。
运行原理…
在向字段的digits属性中添加字符串值时,Odoo在小数精度模型的Usage字段中查找该字符串,返回一个16位精度的元组以及在配置中所定义的小数位数。使用字段定义来代替硬编码,可心让终端用户根据需求来自行配置。
📝小贴士:使用v13之前的版本,要求对浮点字段的digits属性做更多的配置工作。老版本中,小数精度位于一个单独的模块decimal_precision中。要在字段中启用自定义小数精度,需要像这样使用decimal_precision中的get_precision()方法:cost_price = fields.Float( ‘Book Cost’, digits=dp.get_precision(‘Book Price’))。
向模型添加货币字段
Odoo对于与币种相关的货币字段值有特别的支持。我们来学习如何在模型中使用它。
准备工作
我们将继续使用前一节中的my_library插件模块。
如何实现…
货币字段需要一个补充的币种字段来存储相应货币金额。
my_library中已经有了models/library_book.py并定义了一个基础模型。我们将编辑该文件来添加所需的字段:
- 添加所要使用的字段来存储币种:
1234class LibraryBook(models.Model):# ...currency_id = fields.Many2one('res.currency', string='Currency') - 添加货币字段来存储金额:
123456class LibraryBook(models.Model):# ...retail_price = fields.Monetary('Retail Price',# optional: currency_field='currency_id',)
现在升级这个插件模块,模型中即可使用新增字段了。未在视图添加时它们还不会在视图中显示,但通过Settings > Technical > Database Structure > Model查看模型字段(搜索library.book)可确定添加是否成功。在将它们添加到表单视图中之后,效果如下:
运行原理…
货币字段与浮点字段类似,但因通过第二个字段知道了币种,Odoo可以在用户界面中进行正确的展示。
这个币种字段一般称为currency_id,但我们可以使用任意字段名,只要在currency_field可选参数中使用它即可。
📝小贴士:如果以currency_id名称的字段存储币种信息可以在货币字段中省略掉currency_field属性。
在需要在同一记录中维护不同币种的金额时这会非常有用,例如,在我们想要包含销售订单的币种和公司的币种时。可以配置两个 fields.Many2one(res.currency) 字段并对第一个金额使用前一个字段、第二个金额使用第二个字段。
你还应知道金额的小数精度来自币种的定义(res.currency模型中的decimal_precision字段)。
向模型添加关联字段
Odoo模型之间的关联通过关联字段来体现。有三种不同的关联类型:
- many-to-one, 常缩写为m2o
- one-to-many, 常缩写为o2m
- many-to-many, 常缩写为m2m
以图书应用为例,我们知道每本书只有一个出版社,因此在图书和出版社之间可以使用 many-to-one 关联。
而每个出版社都可以出版多本书。因此对前面的many-to-one 关联表示存在一个反向的one-to-many 关联。
最后,某些情况下我们会有many-to-many关联。在本例中,每本书可以有多个作者。而反过来每位作者也可以写多本书。从任意一方看,这都是一个many-to-many关联。
准备工作
我们将继续使用前一节中的my_library插件模块。
如何实现…
Odoo使用伙伴模型res.partner来表示人、组织和地址。对于作者和出版社我们应使用它。我们将编辑models/library_book.py文件来添加这些字段:
- 向图书模型添加图书出版商的many-to-one字段:
123456789class LibraryBook(models.Model):# ...publisher_id = fields.Many2one('res.partner', string='Publisher',# optional:ondelete='set null',context={},domain=[],) - 为出版社的书籍添加one-to-many字段,我们需要继承partner模型。为进行简化,我们在同一Python文件中添加:
12345class ResPartner(models.Model):_inherit = 'res.partner'published_book_ids = fields.One2many('library.book', 'publisher_id',string='Published Books')📝我们这里使用的_inherit属性用于继承已有模型。这一点会在本章后面的使用继承向模型添加功能一节中进行讲解。
- 我们已在图书和作者之间创建了一个many-to-many关联,让我们再次查看一下:
1234class LibraryBook(models.Model):# ...author_ids = fields.Many2many('res.partner', string='Authors') - 相同的关联,但是作者对图书的关联,应加入到partner模型中:
1234567class ResPartner(models.Model):# ...authored_book_ids = fields.Many2many('library.book',string='Authored Books',# relation='library_book_res_partner_rel' # optional)
此时升级该插件模块,新字段就可以在模型中使用了。需要先将它们添加到视图中才会显示,但在开发模式下我们可以通过Settings > Technical > Database Structure > Models来查看模型字段是否添加成功。
运行原理…
Many-to-one字段向模型的数据表中添加了一列,存储关联记录的数据库ID。在数据库级别上,还会创建外键约束,确保保存的ID是对关联表中记录的有效引用 。对这些关联字段不会创建数据库索引,但这可通过添加 index=True 属性来进行完成。
我们可以看到对many-to-one字段还可以使用另外的4个属性。ondelete属性决定在关联记录删除时执行什么操作。例如,在出版社记录删除后图书会怎么样?默认值为’set null’,,会将该字段置为空值。也可以为’restrict’,会阻止关联字段的删除,或者是 ‘cascade’,这会导致关联的字段同样被删除。
最后的两个属性(context和domain)对其它的关联字段同样有效。这些大多在客户端更具意义,在模型层次上,它们作为会在客户端视图中使用的默认值。
- 在点击字段进入关联记录视图时context会向客户端上下文添加变量。例如,我们可以使用它来为新记录设置通过该视图创建的默认值。
- domain是用于限制可用的关联记录列表的搜索过滤器。
context和domain都将在第十章 后端视图中进行更详细的讲解。
One-to-many字段是many-to-one的反向关联,虽然它们像其它字段一样添加在模型中,在数据库中并没有实际的体现。他们仅是编程捷径,启用视图来展现这些关联记录列表。
Many-to-many关联也不会向模型数据表添加列。这类关联在数据库中使用中间关联表进行体现,其中有两列分别存储这两个关联的ID。在图书和作者之间添加新关联在这个关联表中使用图书ID和作者ID创建一条新记录。
Odoo自动处理这一关联表的创建。关联表的名称默认使用两个关联模型名按字母排序加上一个_rel后缀来创建。但我们可以使用relation属性来进行覆盖。
ℹ️需要考虑的一种情况是两个表名过长导致自动生成的数据库标识符超过PostgreSQL的上限63个字符。按照经验,如果两个关联的表名超过23个字符时,应使用relation属性来设置一个更短的名称。下一节中,我们将进行更深入的讨论。
扩展知识…
Many2one字段支持一个额外的auto_join属性。这个标记允许ORM对这个字段使用SQL连接(join)。因此它不受普通的ORM限制,如用户访问控制和记录权限规则。在具体的用例中,它可以解决性能问题,但建议尽量避免使用。
我们讲解了定义关联字段的最简短的方式。下面来看针对这一字段类型的具体属性。
One2many的字段属性如下:
- comodel_name:这是目标模型标识符,对所有关联字段是强制的,但可以在对应位置定义而无需使用关键字
- inverse_name:它仅应用于One2many,是反向Many2one关联的目标模型中的字段名
- limit:它在One2many和Many2many中使用,对在用户界面级别上用于记录读取的数量设置可选限制
Many2many的字段属性如下:
- comodel_name:它的功能与One2many字段中相同
- relation:这是用于支持关联的数据表的名称,覆盖自动定义的名称
- column1:这是连接这个模型的关联表中的Many2one字段的名称
- column2:这是在关联数据表中连接comodel的Many2one字段的名称
对于Many2many在大多数情况下,ORM会处理这些属性的默认值。它甚至可以监测反向的Many2many关联,监测已有关联表及恰当地对调column1和column2的值。
但是,有两种情况我们需要介入并为这些属性提供自己的值。
- 一种情况是我们需在相同的两个模型中添加一个以上的Many2many关联。这时,我们必须为第二个关联提供关联表名,且应与第一个关联不同。
- 另一种情况是在自动生成的关联数据表名长度超过PostgreSQL对于数据对象名的上限63个字符时。
自动生成的关联表名为<model1>_<model2>_rel。但关联表还会为这一关联名创建一个主键索引,标识符如下:
1 |
<model1>_<model2>_rel_<model1>_id_<model2>_id_key |
这个主键也需要满足63个字符的上限。因此,如果两个表名组合起来超过63个字符,会无法满足这一上限并需要手动设置relation属性。
向模型添加等级
等级(Hierarchy) 的表现就像是模型与自身存在着关联,每条记录在相同模型中有一条父级记录以及多条子记录。这只需通过在模型和自身之间建立many-to-one关联来进行实现。
但是,Odoo通过使用嵌套集合模型来对这类字段提供更好的支持。在启用后,在它们的域过滤器中使用child_of运算符进行查询会显著地提升速度。
继续使用图书示例,我们将创建一个等级分类树来用于图书分类。
准备工作
我们将继续使用前一节中的my_library插件模块。
如何实现…
我们会为分类树新建一个Python文件models/library_book_categ.py,如下:
- 在models/__init__.py中载入如下行来加载新的Python代码文件:
1from . import library_book_categ - 创建models/library_book_categ.py 文件并加入如下代码来为图书分类模型创建父子关联:
123456789101112from odoo import models, fields, apiclass BookCategory(models.Model):_name = 'library.book.category'name = fields.Char('Category')parent_id = fields.Many2one('library.book.category',string='Parent Category',ondelete='restrict',index=True)child_ids = fields.One2many('library.book.category', 'parent_id',string='Child Categories') - 同时添加如下代码来启动特别的等级支持:
123_parent_store = True_parent_name = "parent_id" # optional if field is 'parent_id'parent_path = fields.Char(index=True) - 在模型中添加如下行来新增一个防止循环关联的检查:
123456from odoo.exceptions import ValidationError...@api.constrains('parent_id')def _check_hierarchy(self):if not self._check_recursion():raise models.ValidationError('Error! You cannot create recursive categories.') - 这时,我们需要向图书分配一个分类。我们将在library.book模型中新增一个many2one字段进行实现:
1category_id = fields.Many2one('library.book.category')
最后,升级模型来让这些修改生效。
📝要在用户界面中显示library.book.category模型,需要添加菜单、视图和权限规则。更多相关内容请参见第三章 创建Odoo插件模块。你也可通过访问GitHub 仓库来获取代码。
运行原理…
第1和第2步中新建了一个带有等级关联的模型。Many2one关联添加了一个引用父级记录的字段。为进行更快速的子记录发现,这个字段使用index=True参数在数据库中进行了索引。parent_id字段应将ondelete设置为’cascade’ 或’restrict’。到这里,我们拥有了实现等级结构所需的所有内容,但还需要做一些增添来对其进行改善。One2many关联不会在数据库中添加额外的字段,但提供了一个访问父级为该记录的所有记录的快捷方式。
在第3步,我们启动了对于等级的特别支持。这对于高读取低写入指令非常有用,因为它通过更大的写入运算开销带来了更快速的数据浏览。这通过添加一个帮助字段parent_path及设置模型属性为 _parent_store=True来实现。在启用了这个属性之后,该帮助字段会用于在等级树的搜索中存储数据。默认,它假定记录的父级字段名为parent_id,但也可以使用不同的名称。这种情况下,正确的字段名应使用额外的模型属性_parent_name来进行表明。默认值如下:
1 |
_parent_name = 'parent_id' |
推荐使用第4步来防止等级中的循环依赖,即在上级树和下级树中都包含同一条记录。这对于通过树导航的程序非常的危险,因为会进入到无限死循环。models.Model为此提供了一个工具方法(_check_recursion),我们在这里进行了复用。
第5步为向library.book添加一个类型为many2one的category_id字段,这样我们可以对图书记录设置分类。这个只是为完成我们的示例。
扩展知识…
这里所展示的技术应该用于静态等级,即经常进行读取和查询但更新却不频繁。图书分类是一个很好的示例,因为图书馆不会持续地新建分类,但读者会经常将搜索限定到分类或子分类中。这么说的原因是实现是在数据库中的嵌套集合模型中,要求在插入、删除或修改分类时更新所有记录的parent_path列(以及相关联的数据库索引)。那会非常耗资源,尤其是在并行事务中执行多个编辑的情况下。
📝小贴士:如果你在处理极为动态的等级结构,标准的parent_id和child_ids关联会通过避免表级锁来达成更好的性能。
向模型添加约束验证
模型可拥有阻止它们输入不想要的条件的验证。
Odoo支持两种不同类型的约束:
- 数据库级别的约束检查
- 服务端级别的约束检查
数据库级别的约束由PostgreSQL所支持的约束进行限制。最常用的是UNIQUE约束,但也可使用CHECK和EXCLUDE约束。如果这还无法满足需求,可以编写Python代码来使用Odoo服务端级别的约束。
我们将使用第三章 创建Odoo插件模块中所创建的图书模型,并向其添加一些约束。我们会添加一个数据库约束来防止重复的书名,以及一个 Python 模型约束来防止使用未来的日期作为发行日期。
准备工作
本节中,我们继续使用上一节中的在library.book模型。我们预期它至少应包含如下内容:
1 2 3 4 5 |
from odoo import models, fields class LibraryBook(models.Model): _name = 'library.book' name = fields.Char('Title', required=True) date_release = fields.Date('Release Date') |
如何实现…
我们将在models/library_book.py Python文件中编辑LibraryBook类:
- 添加模型属性来创建数据库约束:
123456class LibraryBook(models.Model):# ..._sql_constraints = [('name_uniq', 'UNIQUE(name)', 'Book title must be unique.'),('positive_page', 'CHECK(pages>0)', 'No. of pages must be positive')] - 添加一个模型方法来创建Python代码约束:
12345678910from odoo import api, modelsfrom odoo.exceptions import ValidationErrorclass LibraryBook(models.Model):# ...@api.constrains('date_release')def _check_release_date(self):for record in self:if record.date_release andrecord.date_release > fields.Date.today():raise models.ValidationError('Release date must be in the past')
在对这些代码文件进行修改后,需要升级模块并重启服务。
运行原理…
第1步在模型表中创建了一个数据库约束。这是在数据库级别进行的强制。_sql_constraints模型属性接收一个待创建的约束列表。每个约束由一个三个元素的元组定义,如下所示:
- 约束标识符所使用的后缀。本例中,我们使用了name_uniq,产生的约束名称为library_book_name_uniq。
- PostgreSQL中用于修改或创建数据表的SQL指令。
- 在违反约束时向用户报出的消息。
本例中,我们使用了两个SQL约束。第一个是唯一书名,第二个是检测页数是否为正数。
⚠️警告:如果通过模型继承向已有模型添加约束的话,请确保不存在违反约束的数据行。如果存在这类数据,则无法添加SQL约束,在日志中会报错。
我们在前面已经提到,也可以使用其它数据表约束。注意字段级约束如NOT NULL不能以这种方式进行使用。有关PostgreSQL的通用约束以及具体的数据表约束更详细的信息,请参见http://www.postgresql.org/docs/current/static/ddl-constraints.html。
在第2步中,我们添加了一个方法来执行Python代码验证。它使用了@api.constrains装饰器,表示在参数列表中字段发生变化时应执行它来运行检查 。如果检查失败,会抛出一个ValidationError异常。
扩展知识…
通常如果有复杂的验证约束,可以使用@api.constrains,但对于简单用例,也可以使用带有CHECK选项的_sql_constraints。看看下面的示例:
1 2 3 |
_sql_constraints = [ ('check_credit_debit', 'CHECK(credit + debit > 0 AND credit * debit = 0)', 'Wrong credit or debit value in accounting entry!'), ] |
上例中我们使用了CHECK选项,并使用AND运算符在同一约束中进行多条件检测。
向模型添加计算字段
有时,我们的字段需要通过计算获取值或从相同记录或关联记录中的值获取值。一个典型的示例是总价,由单价乘以数量计算所得。在Odoo模型中,可使用计算字段来实现。
为展示如何使用示计算字段,我们将向图书模型中添加一个字段来计算图书的发行天数。
也可以让计算字段可编辑和可搜索。我们会在示例中进行实现。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
我们将编辑models/library_book.py代码文件来新增一个字段及支持它的逻辑的方法:
- 首先向图书模型添加一个新字段:
12345678910class LibraryBook(models.Model):# ...age_days = fields.Float(string='Days Since Release',compute='_compute_age',inverse='_inverse_age',search='_search_age',store=False, # optionalcompute_sudo=False # optional) - 然后,添加计算逻辑的方法:
1234567891011121314# ...from odoo import api # if not already imported# ...class LibraryBook(models.Model):# ...@api.depends('date_release')def _compute_age(self):today = fields.Date.today()for book in self:if book.date_release:delta = today - book.date_releasebook.age_days = delta.dayselse:book.age_days = 0 - 要添加方法及实现写入计算字段的逻辑,使用如下代码:
123456789from datetime import timedelta# ...class LibraryBook(models.Model):# ...def _inverse_age(self):today = fields.Date.today()for book in self.filtered('date_release'):d = today - timedelta(days=book.age_days)book.date_release = d - 使用如下代码实现允许在计算字段中进行搜索的逻辑:
123456789101112131415from datetime import timedeltaclass LibraryBook(models.Model):# ...def _search_age(self, operator, value):today = fields.Date.today()value_days = timedelta(days=value)value_date = today - value_days# 运算符转换:# age_days > value -> date < value_dateoperator_map = {'>': '<', '>=': '<=','<': '>', '<=': '>=',}new_op = operator_map.get(operator, operator)return [('date_release', new_op, value_date)]
需升级模块并重启Odoo来正确地启用这些新条件。
运行原理…
计算字段的定义和普通字段一致,不同的是添加了一个compute属性来指定用于计算的方法名。
它们的相似性带有欺骗性,因为计算字段的内部与普通字段有非常大的不同。计算字段是在运行时动态计算的,因此不在数据库中存储、默认无法对计算字段进行写操作。你需要自己对计算字段添加可写、可搜索的支持。下面看如何实现。
计算函数在运行时动态计算,但ORM使用缓存来避免在每次访问值时的低效重计算。因此,它需要知道所依赖的其它字段。它使用@depends装饰器来监测缓存值何时应置为无效并重新计算。
ℹ️确保compute函数总是为计算字段设置一个值。否则会抛出错误。这在代码中包含if条件而对计算字段设置值失败时会发生。那样会很难进行调试。
写操作可通过实现inverse函数来添加。使用分配给计算字段的值来更新原字段。当然,这只在较简单的计算中有意义,但还是有些用例可以用到它的。在我们的示例中, 我们让通过编辑Days Since Release计算字段来设置图书发行日期成为可能。inverse是可选属性,如果不想让该计算字段可编辑,可以忽略它。
也可以通过将search属性设置为方法名(类似compute和inverse)来让非存储的计算字段可搜索。类似inverse,search也是可选属性,如果不想让该计算字段可搜索,可以忽略它。
但是,这个方法预期不实现实际的搜索。而是接收用于搜索该字段的运算符和值来作为参数,并应返回一个带有用于替换搜索条件的域。本例中,我们将一个发行天数字段的搜索转换为发行日期字段上等价的搜索条件。
可选的store=True标记存储数据库中的字段。在这种情况下,执行计算后字段值会存储到数据库中,此后它们会像普通字段一样进行获取,而不是运行时重新计算。借助@api.depends装饰器,ORM会知道何时需要重新计算并更新这些存储值。你可以把它看作一个持久缓存。它还具有可将该字段作为搜索条件的好处,包括排序和分组的操作。如果在计算字段中使用store=True,则无需实再现search方法,因为字段已存储在数据库中,可以直接根据存储字段搜索、排序。
compute_sudo=True标记用于需要提权来执行计算的情况。这种情况可能是计算时需要使用终端用户无法访问的数据。
📝重要小记:Odoo v13中compute_sudo的默认值发生了改变。在Odoo v13之前,compute_sudo的值为False。但在v13中,compute_sudo的默认值基于store属性。如果store属性为True,则compute_sudo为True,否则为False。但是开发者可以在字段定义放置compute_sudo来显式地手动进行修改。
使用它时需要小心,因为它会跳过权限规则 ,包含多公司设置的按公司分隔的规则。确保反复确认在计算中所使用的域来避免相关的问题。
扩展知识…
Odoo v13对ORM引入了一种新型的缓存机制。此前,根据环境进行缓存,但在Odoo v13中使用了全局缓存。因而,如果存在依赖于上下文值的计算字段,那么有时可能会得到错误的值。可以使用@api.depends_context装饰器解决这一问题。参见如下示例:
1 2 3 4 5 6 |
@api.depends('price') @api.depends_context('company_id') def _compute_value(self): company_id = self.env.context.get('company_id') ... # other computation |
前例中可以看到运算中使用了上下文中的company_id。通过在depends_context装饰器中使用company_id,我们可以确保字段值会根据上下文中company_id的值重新进行计算。
暴露存储在其它模型中的关联字段
在从服务端读取数据时,Odoo客户端仅能获取模型中存在的字段及查询的值。客户端代码不同于服务端,无法使用点号标记来获取关联表中的数据。
但是,这些字段可通过将它们添加为关联字段来进行访问。我们将会让图书模型中的出版社城市可以被访问。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
编辑models/library_book.py文件添加一个新关联字段:
- 确保我们有一个图书出版社的字段(此前示例中已添加过):
1234class LibraryBook(models.Model):# ...publisher_id = fields.Many2one('res.partner', string='Publisher') - 接着,为出版社城市添加一个关联字段:
123456# class LibraryBook(models.Model):# ...publisher_city = fields.Char('Publisher City',related='publisher_id.city',readonly=True)
最后,我们需要升级该插件模块来让新字段在模型中可用。
运行原理…
关联字段和普通字段相似,但是有一个额外的属性related,带有一个分隔字段链遍历的字符串。
在本例中,我们通过publisher_id访问出版社的关联记录,然后读取它的city字段。还可以使用更长的链表达式,例如publisher_id.country_id.country_code。
注意在本节中,我们设置关联字段为只读。如果不这么做,字段将为可写,用户可能会修改其值。这会产生修改关联出版社城市字段值的影响。这可能会既有用又有副作用,在操作时应小心;所有由相同出版社出版的图书的publisher_city都会被更新,这可能会在用户的预料之外。
扩展知识…
关联字段实际上是计算字段。只是提供了一种方便的快捷语法来从关联模型读取字段值。作为一个计算字段,意味着也可以使用store属性。作为快捷方式,它们也拥有引用字段的所有属性,如name, translatable和required。
此外,它支持一个类似compute_sudo的related_sudo标记,在设置为True时,字段链遍历时会在不进行用户权限检查。
📝小贴士:在create()方法中使用关联字段会影响到性能,因此这些字段的计算会延迟到它们创建结束的时候。因此 ,如果有一个One2many关联,如sale.order和sale.order.line模型,又在line模型的关联字段上引用了订单模型的字段,应当在记录创建时在订单模型中显式地读取该字段,而不是使用关联字段快捷方式,尤其是在有很多订单条目(line)时。
使用引用字段添加动态关联
对于引用字段,首先我们需要决定关联的目标模型(或称comodel)。但有时我们会让用户来做决定,首先选定我们所要的模型然后记录想要关联的记录。
在Odoo中这通过使用引用字段来实现。
准备工作
本节中,我们继续使用上一节中的在library.book模型。
如何实现…
编辑models/library_book.py文件来添加新的关联字段:
- 首先,我们需要添加一个帮助方法来动态构建一个可选目标模型列表:
12345678from odoo import models, fields, apiclass LibraryBook(models.Model):# ...@api.modeldef _referencable_models(self):models = self.env['ir.model'].search([('field_id.name', '=', 'message_ids')])return [(x.model, x.name) for x in models] - 然后,我们需要添加引用字段并使用上述的函数提供一个可选模型列表:
123ref_doc_id = fields.Reference(selection='_referencable_models',string='Reference Document')
因为我们修改了模型的结构,需要升级模块来使这些修改生效。
运行原理…
引用字段类似于many-to-one字段,不同的是它们允许用户选择要关联的模型。
目标模型可通过由selection属性提供的列表进行选择。selection属性应是一个包含两个元素的元组,第一个元素是模型的内部标识符,第二个是它的描述文本。
以下是一个示例:
1 |
[('res.users', 'User'), ('res.partner', 'Partner')] |
但是,不需要提供一个固定的列表,我们可以使用大部分通用的模型。为进行简化,我们使用带有消息功能的所有模型。使用_referencable_models方法,我们动态地提供了一个模型列表。
本节一开始提供了一个函数来浏览所有模型记录,可供动态引用来创建用于提供给selection属性的列表。虽然两种形式都允许,我们在引号内声明了函数名,而不是不加引号直接引用函数。这更为灵活,比如它允许所引用的函数可以在代码的后面进行定义,在使用直接引用时则不能这么做。
该函数需要一个@api.model装饰器,因为它在模型级别而非记录集级别上进行操作。
📝虽然这个功能看起来很棒,它运行的开销会很大。使用引用字段显示大量记录(如在列表视图中)会带来很重的数据库负载,因为每个值都需在一个单独的查询中进行查找。它也不能像常规关联字段那样利用数据库的引用一致性。
使用继承向模型添加功能
Odoo一个最重要的功能是模块插件可以继承其它模块插件中定义的功能,而又无需编辑原功能中的代码。可以是添加字段或方法,修改已有字段或继承已有方法来执行额外的逻辑。
根据官方文档所说,Odoo提供三种类型的继承:
- 类继承(扩展)
- 原型继承
- 代理继承
我们会通过不同的小节学习这些继承。本节中我们学习类型继承(扩展)。用于对已有模型添加新字段或方法。
我们将继承内置的客户模型res.partner来添加所著书数量的计算字段。包含对已有模型添加一个字段或一个方法。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
我们将继承内置的Partner模型。读者可能还记得我们在本章的向模型添加关联字段一节中继承过res.partner。为了简化讲解,我们将复用 models/library_book.py代码文件中的res.partner模型:
- 首先,我们将确保在Partner模型中有authored_book_ids反向关联并添加计算字段:
1234567class ResPartner(models.Model):_inherit = 'res.partner'_order = 'name'authored_book_ids = fields.Many2many('library.book', string='Authored Books')count_books = fields.Integer( 'Number of Authored Books',compute='_compute_count_books' ) - 然后,添加需要用于计算图书数量的方法:
12345678# ...from odoo import api # if not already imported# class ResPartner(models.Model):# ...@api.depends('authored_book_ids')def _compute_count_books(self):for r in self:r.count_books = len(r.authored_book_ids)
最后,我们需要升级这个插件模块来让修改生效。
运行原理…
在模型类通过_inherit属性进行定义时,它向所继承模型添加了修改,而没有进行替换。
这意味着在继承类中定义的字段会在父级模型中新增或修改。在数据库层,ORM对同一张数据表添加字段。
字段也被增量修改。表示如果该字段在父类中已存在,仅修改在继承类中声明的属性,其它的保持原有父类中的内容不变。
在继承类中定义的方法替换父类中的方法。如果你不通过super调用触发父级方法,那么父级版本的方法则不会被调用,我们也就不拥有该项功能。因此,通过继承在已有方法中添加新逻辑时,应包含一个带有super的语句来调用其父类中的方法。这部分在第五章 基本服务端开发中做进一步的讲解。
📝本节会向已有模型新增字段。如果想在已有视图(用户界面)添加这些新字段的话,参见第九章 后端视图中的修改已有视图 – 视图继承一节。
扩展知识…
通过_inherit经典继承,也可以将父级模型的功能拷贝到一个全新的模型中。这通过添加一个在带有不同标识符的_name类属性来实现。以下是一个示例:
1 2 3 |
class LibraryMember(models.Model): _inherit = 'res.partner' _name = 'library.member' |
新模型有其自己的数据表,包含完全独立于res.partner父模型的自身数据。因其仍继承Partner模型,此后的任意修改也会影响到新模型。
在官方文档中,这被称为原型继承,但在实践中鲜有使用。原因在于代理继承通常可以更高效的方式满足了这一需求,也无需复制数据结构。参见本章中的使用代理继承将功能拷贝至另一个模型一节了解更多内容。
使用继承拷贝模型定义
在上一节中我们学习了类继承(扩展)。下面我们学习原型继承,用于从已有模型中拷贝整个定义。本节中,我们会做一个library.book模型的拷贝。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
原型继承通过同时使用_name和_inherit 属性来实现。执行如下步骤来生成对library.book模型的拷贝:
- 在/my_library/models/目录下新建一个library_book_copy.py 文件:
- 在library_book_copy.py 文件中添加如下代码:
123456from odoo import models, fields, apiclass LibraryBookCopy(models.Model):_name = 'library.book.copy'_inherit = 'library.book'_description = "Library Book's Copy" - 在/my_library/models/__init__.py文件导入一条新的文件引用。修改后的__init__.py文件内容如下:
123from . import library_bookfrom . import library_book_categfrom . import library_book_copy
最后,我们需要升级插件模块来让修改生效。查看新模型定义,可访问Settings > Technical > Database Structure > Models菜单。在这里会看到一个新的模型library.book.copy。
📝小贴士:要想查看新模型的菜单和视图,需要添加视图及菜单的XML定义。更多有关菜单和视图的相关内容,参见第三章 创建Odoo插件模块中的添加菜单项和视图一节。
运行原理…
能过同时使用类属性_name和_inherit,可以拷贝模型的定义。在模型中使用这两个属性时,Odoo会拷贝_inherit的模型定义,创建一个带有_name属性的新模型。
本例中,Odoo会拷贝library.book模型的定义,创建一个新模型library.book.copy。新的library.book.copy模型有一个包含自身数据的数据表,与父级模型library.book完全独立开来。因其依然继承该父模型,后续的变更还是会影响到这一新模型。
原型继承复制父类中的所有属性。会拷贝字段、属性和方法。如果想在子类中进行修改,只需在子类中添加新的定义。例如,library.book模型有一个_name_get方法。如果希望在子类中使用不同版本的_name_get,需要在library.book.copy模型中重新定义该方法。
⚠️警告:在_name和_inherit属性中使用同一个模型名称时原型继承无法生效。如果在_name和_inherit属性中确实使用了相同的模型名称,会和普通扩展继承表现一致。
扩展知识…
官方文档称其为原型继承,但实际开发中很少使用到。原因是代理继承通常会以更高效的方式满足相应的需求,而又无需复制数据结构。更多相关信息,参见下一小节使用代理继承将功能拷贝至另一个模型。
使用代理继承将功能拷贝至另一个模型
第三种继承类型是代理继承。使用的不是_inherit,而是_inherits类属性。有时我们不要修改已有模型,希望根据已有模型创建一个新模型来使用原有功能。这时可以通过拷贝原型继承中的模型定义,但会导致重复的数据结构。如果希望拷贝模型定义而又不复制数据结构,那么答案就是Odoo的代理继承,使用_inherits模型属性(注意这里多一个 s)。
传统继承与面向对象编程的概念有很大不同。而代理继承则与其相似,其中可创建一个新的模型来包含父级模型中的功能。它还支持多态继承,这时从两个或多个其它的模型中进行继承。
我们的图书馆中已经有书了。是时候修改让图书馆也拥有会员了。对于图书会员,我们需要Partner模型中的所有身份和地址数据,也会想要保留一些有关会员的信息:起始日期、结束日期和会员卡号。
向Partner模型添加这些字段不是最好的方案,因为对于非会员的成员们无需使用到这些。使用一个带有额外字段的新模型继承Partner模型则会非常好。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
新图书会员模型应有自己的独立Python代码文件,但为保持讲解尽可能简单,我们将复用models/library_book.py文件:
- 添加新模型继承res.partner:
123456class LibraryMember(models.Model):_name = 'library.member'_inherits = {'res.partner': 'partner_id'}partner_id = fields.Many2one('res.partner',ondelete='cascade') - 接下来,我们将添加针对图书会员的字段:
123456# class LibraryMember(models.Model):# ...date_start = fields.Date('Member Since')date_end = fields.Date('Termination Date')member_number = fields.Char()date_of_birth = fields.Date('Date of birth')
此时,我们应升级该插件模型来让修改生效。
运行原理…
_inherits模型属性设置我们想要继承的父级模型。本例中只有一个 – res.partner模型。它的值是一个键值对字典,键是被继承的模型,而值是用于关联它们的字段名。这些是我们必须同时在模型中定义的Many2one字段。本例中,partner_id是用于关联父级模型Partner的字段。
为更好理解它如何运行,我们来看在新建会员时数据库级别上会发生什么:
- 在res_partner表中新建一条记录
- 在library_member表中新建一条记录
- library_member表中的partner_id字段设置为所创建的res_partner记录的id
会员记录自动关联到一个新的Partner记录。它仅是一个 many-to-one关联,但代理机制注入了一些魔力来让Partner的字段看起来就好像属于Member的记录一样,新的Partner记录会自动和新的会员记录一同创建。
你可能会想要知道这个自动创建的Partner记录并没有什么特别的。这是一个常规的Partner,如果查看Partner模型,就会看到这条记录(当然其中不包含那些额外的会员数据)。所有的会员都是成员(Partner),但只有部分成员是会员。
那么在删除同时还是会员的成员时会发生什么呢?可通过关联字段的ondelete值来进行决定。对partner_id我们使用了cascade。这表示删除成员会同时删除对应的会员。我们可以使用更为保守的设置restrict来禁止在有关联会员时删除成员。这样的话只有删除会员时才会有效。
需要注意代理继承仅用于字段,而不能用于方法。因此,如果Partner模型有一个do_something()方法,成员模型不会自动继承它。
扩展知识…
对于这个继承代理有一个快捷方式。代替创建一个_inherits字典,可以在Many2one字段定义中使用delegate=True属性。这和_inherits选项的功能完全一样。其主要优点是更为简洁。在给出的示例中,我们执行了与前述相同的继承代理 ,但在这种情况下,我们对partner_id字段使用了delegate=True选项来代替_inherits字典的创建:
1 2 3 4 5 6 7 |
class LibraryMember(models.Model): _name = 'library.member' partner_id = fields.Many2one('res.partner', ondelete='cascade', delegate=True) date_start = fields.Date('Member Since') date_end = fields.Date('Termination Date') member_number = fields.Char() date_of_birth = fields.Date('Date of birth') |
关于代理继承一个值得注意的用例是用户模型 res.users。它继承自成员(res.partner)。这表示其中在User中可见的一些字段实际存储在Partner模型中(尤其是name字段)。在新用户创建时,我们还获取了一个新的自动创建的Partner。
还应说明带有_inherit的传统继承会将功能拷贝到新模型中,虽然效率并不高。这在使用继承向模型添加功能一节中进行了讨论。
使用抽象模型实现可复用模型功能
有时,会有一个具体的功能,我们希望添加到几个不同的模型中。在不同的文件中重复相同代码基本上是一种不良编程实践,最好可以一次实现多次复用。
抽象模型让我们可以创建一个通用模型来实现一些功能,然后由普通模型进行继承以使用该功能。
作为示例,我们将实现一个简单的存档功能。它将active字段加入到模型中(如果尚未存在)并添加一个存档方法来切换active标记。这可以生效是因为active是一个魔法字段,如果默认在模型中出现,active=False 的记录会在查询中被过滤掉。
下面我们将在图书模型中添加它。
准备工作
本节中,我们继续使用上一节中的my_library插件模型。
如何实现…
存档功能显然可独立为一个插件模块或者至少应有自己的Python代码文件。但为保持讲解尽可能简单,我们将会把它塞到models/library_book.py文件中:
- 为存档功能添加抽象模型。应在使用它的图书模型中定义:
123456class BaseArchive(models.AbstractModel):_name = 'base.archive'active = fields.Boolean(default=True)def do_archive(self):for record in self:record.active = not record.active - 接着,我们编辑图书模型来继承存档模型:
1234class LibraryBook(models.Model):_name = 'library.book'_inherit = ['base.archive']# ...
需要对插件模块进行升级来让修改生效。
运行原理…
抽象模型基于models.AbstractModel的类进行创建,而非常用的models.Model。它拥有常规模型的所有属性和功能,区别在于ORM不会在数据库中创建实际的体现。这表示它不能存储任何数据。仅用作添加到常规模型中的可复用功能的一个模板。
我们的存档抽象模型非常简单,仅添加active字段和一个方法来切换active标记的值,我们将在稍后在用户界面中通过按钮进行使用。
模型类中定义了_inherit属性时,它继承那些类中的属性方法,定义在当前类中的属性方法对这些继承功能进行修改。
这里所采用的机制与常规模型继承相同(如使用继承向模型添加功能一节)。你可能注意到了_inherit使用一个模型标识符列表而不是带有一个模型标识符的字符串。其实_inherit可以使用这两种形式。使用列表形式允许我们继承多个(通常是抽象)类。在本例中,我们仅继承了一个类,因此使用文本字符串也没有问题。为进行演示我们使用了列表。
扩展知识…
值得注意的内置抽象模型是mail.thread,这由mail(Discuss)插件模块提供。在模型中它启用讨论功能来驱动在不同表单底部看到的消息墙。
AbstractModel外,还有第三种模型类型:models.TransientModel。像models.Model它有一个数据库体现,但所创建的记录供临时使用,会定期由服务端调度任务清除。除此之后,临时模型(也译作瞬态模型)和常规模型的功能一致。
models.TransientModel对于称之为向导的更为复杂的用户交互会非常有用。向导用于请求用户输入。在第八章 高级服务端开发技巧中,我们探讨如何使用它们来实现高级用户交互。