Odoo 12开发者指南第五章 应用模型

全书完整目录请见:Odoo 12开发者指南(Cookbook)第三版

本章中我们将讲解如下小节:

  • 定义模型表现及顺序
  • 向模型添加数据字段
  • 使用可配置精度的浮点字段
  • 向模型添加货币字段
  • 向模型添加关联字段
  • 向模型添加等级
  • 向模型添加约束验证
  • 向模型添加计算字段
  • 暴露存储在其它模型中的关联字段
  • 使用引用字段添加动态关联
  • 使用继承向模型添加功能
  • 为可复用模型功能使用抽象模型
  • 使用代理继承将功能拷贝至另一个模型

引言

本章中的各小节会对已有的那个插件模型做一些小的新增。我们将使用在第四章 创建Odoo插件模块中所创建的模块。

技术准备

要按照本章中的示例进行操作,你应该要有一个第四章 创建Odoo插件模块中所创建的模块并且该模型应可用。

本章中使用的代码可以在GitHub仓库中进行下载,地址为https://github.com/alanhou/odoo12-cookbook/tree/master/Chapter05。

观看如下视频来查看实时代码操作:http://t.cn/E9ZHCPR

定义模型表示及顺序

模型中有结构性属性来定义它们的行为。这是以下划线作为前缀。最重要的属性是_name,因为这定义了内部全局标识符。Odoo通过这一_name属性来创建数据表。例如,如果你使用_name="library.book",那么Odoo ORM会在数据库中创建一张library_book数据表。这也是为什么_name必须在Odoo系统中要保持唯一。

在模型中可以使用另外两个属性:一个设置用于记录展示或标题的字段,另一个设置记录的展现的顺序。

准备工作

这一节中我们假定你已经有一个包含my_library模块的实例,如第四章 创建Odoo插件模块中所描述。

如何操作...

my_library实例应包含一个名为models/library_book.py的Python文件,它定义一个基础模型。我们将编辑该文件来在_name之后添加一个新的类级别的属性:

  1. 加入如下代码来添加一个用户友好的模型标题:
_description = 'Library Book'
  1. 首先对记录进行排序(按时间最近排序,然后按标题排序),添加如下代码:
_order = 'date_release desc, name'
  1. 添加如下代码来使用short_name字段作为记录的表示:
_rec_name = 'short_name'
    short_name = fields.Char('Short Title', required=True)
  1. 在表单视图中添加short_name字段,这样会在该视图中显示这一新字段:

完成如上操作之后,我们library_book.py文件应该是下面这样:

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 文件中的

视图应该是下面这样的:


    
        
            
            
        
        
            
            
        
    

然后我们应升级模块让这些更改在Odoo中生效。

运行原理...

第一步为定义的模型添加了一个用户友好的标题。这并非强制的,但可以为一些插件所用。例如,它可在创建记录时用于mail插件模块中追踪功能的通知文本。更多详情,请参见第二十三章 在Odoo中管理email。

默认Odoo使用内部id值进行排序。但是,可对其进行修改来使用我们自己选择的字段排序,做法是提供一个包含字段名逗号分隔列表字符串的_order属性。字段名后可接desc关键字来进行降序排序。

仅能使用存储在数据库中的字段。未存储的计算字段无法用于记录排序。

ℹ️_order字符串的语法类似于SQL中的ORDER BY语句,但进行了简化。例如,不允许NULLS FIRST这样的特殊语句。

模型记录在其它记录中引用时使用了一种表现形式。例如,带有值1的user_id表示管理员用户。在表单视图中显示时,Odoo会显示用户名,而非数据库ID。默认使用的是name字段。事实上这是_rec_name属性的默认值,这也是为什么在我们的模型中添加name字段会很方便。

如果模型中不存在name字段,会通过模型和记录标识符生成一个描述,类似(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),我们可以进行如下定义:

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

向模型添加数据字段

模型用于存储数据,这些数据以字段进行结构化组织。这里,你将学习到可存储在字段中的不同数据类型,以及如何在模型中进行添加。

准备工作

这一节假定你已经有一个如第四章 创建Odoo插件模块中所述的带有my_library插件模型的实例准备就绪。

如何操作...

my_library插件模型应该已经有定义了基本模型的models/library_book.py文件,我们将编辑该文件来新增字段:

  1. 使用最小化语法来向图书模型添加字段:
from odoo import models, fields
    class 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),
            )
  1. 我们已向模型新增了字段。仍需在表单视图中添加这些字段来在用户界面中反映出这些修改。参见如下在表单视图中增加字段的代码:

升级模型会使用Odoo模型中这些修改生效。

查看如下这些不同字段的示例。这里我们对字段使用了不同类型的属性。这会让你对字段声明有一些更好的概念:

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对象的形式对其进行处理。Odoo 12之前的版本中ORM以字符串的形式处理日期。所使用的格式在odoo.fields.DATE_FORMAT中定义。

  • Datetime用于日期时间值。在数据库中以原生UTC时间datetime进行存储。ORM中以Python datetime对象的形式对其进行处理。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标记强制字段在用户界面中默认为必填。

  • sanitize标记用于HTML字段并去除包含不安全标签的内容。使用它会对输入进行全局清理。如果需要更精细的控制,可以使用一些关键字,仅在启用sanitize时生效:

    • sanitize_tags=True删除白名单列表以外的标签(默认项)
    • sanitize_attributes=True删除白名单列表以外的标签属性
    • sanitize_style=True删除白名单列表以外的样式属性
    • strip_style=True删除所有样式元素
    • strip_class=True删除所有class属性

    ℹ️这里所提及的各个白名单列表在odoo/tools/mail.py中定义。

  • company_dependent标记让该字段根据公司存储不同值。它取代了已淘汰的Property字段类型。

最后,我们根据模型中新增的字段更新了表单视图,我们在这里以自己的方式放置标签,但你可以根据自己的需要放置在任意位置。表单视图在第十章 后端视图中会进行细致的讲解。

扩展知识...

Selection字段还接收一个函数引用来替代列表作为selection属性。这允许动态生成选项列表,你会在本章的使用引用字段添加动态关联一节中看到一个示例,其中使用了selection属性。

Date和Datetime字段对象暴露了一些非常方便的工具方法。

Date有如下方法:

  • fields.Date.to_date(string_value将字符串解析为一个date对象。
  • fields.Date.to_string(date_value)将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)。它的定义如下:

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)语句。它默认会限制仅搜索活跃记录。

使用可配置精度的浮点字段

在使用浮点字段时,我们可能会想让终端用户配置所使用的精度。Decimal Precision Configuration模块插件提供了这一功能。

我们将向图书模型字段添加一个带有用户可配置位数的成本价字段。

准备工作

我们将复用在第四章 创建Odoo插件模块中创建的my_library插件模块。

如何操作...

我们需要安装decimal_precision模块,在配置中添加Usage,然后在模型字段中使用它。

  1. 确保安装了数字精度模块,在顶级菜单中选择Apps,删除默认过滤器,搜索Decimal Precision Configuration模块,若未安装则进行安装:


    Odoo 12开发者指南第五章 应用模型_第1张图片
    image.png
  2. 通过Settings菜单下的链接激活开发者模式(参见第一章 安装Odoo开发环境中的激活Odoo开发者工具一节)。这会启用Settings > Technical菜单。

  3. 访问数字精度设置。这需要打开Settings顶级菜单并选择Technical > Database Structure > Decimal Accuracy。我们应该会看到一个当前定义的设置列表。

  4. 添加一个新配置,设置Usage为Book Price并选择数字精度:


    Odoo 12开发者指南第五章 应用模型_第2张图片
    image.png
  5. manifest.py声明文件中添加这个新依赖。如下所示:

{ 
        'name': 'Chapter 05 code',
        'depends': ['base', 'decimal_precision'],
        'data': ['views/library_book.xml'] 
    }
  1. 要使用数字精度设置添加模型字段,编辑models/library_book.py文件并添加如下代码:
from odoo.addons import decimal_precision as dp
    class LibraryBook(models.Model):
        cost_price = fields.Float(
            'Book Cost', dp.get_precision('Book Price'))

小贴士:不论何时向模型添加新字段,你将需要将它们添加到视图中以在用户界面中访问它们。在前例中,我们添加了cost_price。要在表单视图中看到它,需要添加

运行原理...

get_precision()函数查找数字精度中的Usage字段并返回一个16位精度的元组以及在配置中所定义的小数位数。

在字段定义中使用这一函数来取代硬编码,可以让终端用户根据自己的需求来进行配置。

向模型添加货币字段

Odoo对于与币种相关的货币值有特别的支持。我们来看如何在模型中使用它。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

货币字段需要一个补充的币种字段来存储相应数量的货币值。

my_library中已经有了models/library_book.py并定义了一个基础模型。我们将编辑该文件来添加所需的字段:

  1. 添加所要使用的字段来存储币种:
class LibraryBook(models.Model):
        # ...
        currency_id = fields.Many2one(
            'res.currency', string='Currency')
  1. 添加货币字段来存储数额:
class LibraryBook(models.Model):
        # ...
        retail_price = fields.Monetary(
            'Retail Price',
            # optional: currency_field='currency_id',
        )

现在升级这个插件模块,模型中即可使用新增字段了。未在视图添加时它们还不会在视图中显示,但通过Settings > Technical > Database Structure > Model查看模型字段可确定添加是否成功。在将它们添加到表单视图中之后,会是如下这样:

image.png

运行原理...

货币字段与浮点字段类似,但因通过第二个字段知道了币种,Odoo可以在用户界面中进行正确的展示。

这个币种字段一般称为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关联。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

Odoo使用伙伴模型res.partner来表示人、组织和地址。对于作者和出版社我们应使用它。我们将编辑models/library_book.py文件来添加这些字段:

  1. 向图书模型添加图书出版商的many-to-one字段:
class LibraryBook(models.Model):
        # ...
        publisher_id = fields.Many2one(
            'res.partner', string='Publisher',
            # optional:
            ondelete='set null',
            context={},
            domain=[],
        )
  1. 为出版社的书籍添加one-to-many字段,我们需要继承partner模型。为进行简化,我们将其添加到相同的Python文件中:
class ResPartner(models.Model):
        _inherit = 'res.partner'
        published_book_ids = fields.One2many(
            'library.book', 'publisher_id',
            string='Published Books')
> ℹ️我们这里使用的_inherit属性用于继承已有模型。这一点会在本章后面的使用继承向模型添加功能一节中进行讲解。
  1. 我们已在图书和作者之间创建了一个many-to-many关联,让我们再次查看一下:
class LibraryBook(models.Model):
         # ...
         author_ids = fields.Many2many(
            'res.partner', string='Authors')
  1. 相同的关联,但是作者对图书的关联,应加入到partner模型中:
class 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个字符时。

自动生成的关联表名为__rel。但关联表还会为这一关联名创建一个主键索引,标识符如下:

__rel__id__id_key

这个主键也需要满足63个字符的上限。因此,如果两个表名组合起来超过63个字符,你会无法满足这一上限并需要手动设置relation属性。

向模型添加等级

等级(Hierarchy) 的表现就像是模型与自身存在着关联,每条记录在相同模型中有一个父级记录以及多个子记录。这只需通过在模型和自身之间建立many-to-one关联来进行实现。

但是,Odoo通过使用嵌套集合模型来对这类字段提供更好的支持。在启用后,在它们的域过滤器中使用child_of运算符进行查询会显著的提升速度。

继续使用图书示例,我们将创建一个等级分类树来用于图书分类。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

我们会为分类树新建一个Python文件models/library_book_categ.py,如下:

  1. 在models/init.py中载入如下行来加载新的Python代码文件:
from . import library_book_categ
  1. 创建models/library_book_categ.py 文件并加入如下代码来为图书分类模型创建父子关联:
from odoo import models, fields, api
    class 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')
  1. 同时添加如下代码来启动特别的等级支持:
_parent_store = True
    _parent_name = "parent_id" # optional if field is 'parent_id'
    parent_path = fields.Char(index=True)
  1. 在模型中添加如下行来新增一个防止循环关联的检查:
from 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.')
  1. 这时,我们需要向图书分配一个分类。我们将在library.book模型中新增一个many2one字段进行实现:
category_id = fields.Many2one('library.book.category')

最后,升级模型来让这些修改生效。

ℹ️要在用户界面中显示library.book.category模型,你需要添加菜单、视图和权限规则。更多相关内容请参见第四章 创建Odoo插件模块。你也可通过访问GitHub 仓库来获取代码。

Odoo 12开发者指南第五章 应用模型_第3张图片
image.png

运行原理...

第1和第2步中新建了一个带有等级关联的模型。Many2one关联添加了一个引用父级记录的字段。为进行更快速的子记录发现,这个字段使用index=True参数在数据库中进行了索引。parent_id应将ondelete设置为'cascade' 或'restrict'。到这里,我们拥有了实现等级结构所需的所有内容,但还需要做一些增添来对其进行改善。One2many关联不会在数据库中添加额外的字段,但提供了一个通过将这些记录作为父级来访问所有记录的快捷方式。

在第3步,我们启动了对于等级的特别支持。这是一个非常有用的高读取低写入指令,因为它通过更大的写入运算开销带来了更快速的数据浏览。这通过添加一个帮助字段parent_path及设置模型属性为 _parent_store=True来实现。在启用了这个属性之后,该帮助字段会用于在等级树的搜索中存储数据。默认,它假定记录的父级字段名为parent_id,但也可以使用不同的名称。这种情况下,正确的字段名应使用额外的模型属性_parent_name来进行表明。默认值如下:

_parent_name = 'parent_id'

推荐使用第4步来防止等级中的循环依赖,即在上级树和下级树中都包含同一条记录。这对于通过树导航的程序非常的危险,因为会进入到无限死循环。models.Model为此提供了一个工具方法(_check_recursion),我们在这里进行了复用。

第5步为向library.book添加一个类型为many2one的category_id字段,这样我们可以对图书记录设置分类。这个只是为完成我们的示例。

扩展知识...

这里所展示的技术应该用于静态等级,即经常进行读取和查询但更新却不频繁。图书分类是一个很好的示例,因为图书馆不会持续地新建分类,但读者会经常将搜索限定到分类或子分类中。这么说的原因是实现是在数据库中的嵌套集合模型中,要求在插入、删除或修改分类时更新parent_path列(以及相关联的数据库索引)。那会非常耗资源,尤其是在并行事务中执行多个编辑的情况下。

小贴士:如果你在处理动态等级结构,标准的parent_id和child_ids关联会通过避免表级锁来形成更好的性能。

向模型添加约束验证

模型可拥有阻止它们输入不想要的条件的验证。

可以使用两种不同类型的约束:

  • 数据库级别的约束检查
  • 服务级别的约束检查

数据库级别的约束由PostgreSQL所支持的约束进行限制。最常用的是UNIQUE约束,但也可使用CHECK和EXCLUDE约束。如果这还无法满足需要,可以编写Python代码来使用Odoo服务级别的约束。

我们将使用第四章 创建Odoo插件模块中所创建的图书模型,并向其添加一些约束。我们会添加一个数据库约束来防止重复的书名,以及一个 Python 模型约束来防止使用未来的日期作为发行日期。

准备工作

本节中,我们将在library.book模型中添加约束。为此我们使用第四章 创建Odoo插件模块中的my_library模型。

我们预期它至少应包含如下内容:

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类:

  1. 添加模型属性来创建数据库约束:
class LibraryBook(models.Model):
         # ...
         _sql_constraints = [
         ('name_uniq',
         'UNIQUE (name)',
         'Book title must be unique.')
         ]
  1. 添加一个模型方法来创建Python代码约束:
from odoo import api, models
    from odoo.exceptions import ValidationError
    class LibraryBook(models.Model):
         # ...
         @api.constrains('date_release')
         def _check_release_date(self):
             for record in self:
                 if record.date_release and
                    record.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指令。
  • 在违反约束时向用户报出的消息。

我们在前面已经提到,也可以使用其它数据表约束。注意列级约束如NOT NULL不能以这种方式进行使用。有关PostgreSQL的通用约束以及具体的数据表约束更详细的信息,请参见https://www.postgresql.org/docs/current/ddl-constraints.html。

在第2步中,我们添加了一个方法来执行Python代码验证。它使用了@api.constrains装饰器,表示在参数列表中字段发生变化时应执行它来运行检查 。如果检查失败,会抛出一个ValidationError异常。

扩展知识...

通常如果有复杂的验证约束,可以使用@api.constrains,但对于简单用例,也可以使用带有CHECK选项的_sql_constraints。看下下面的示例,它防止用户添加没有页面数或页面数或负值的图书:

_sql_constraints = [
    ('positive_page', 'CHECK(pages>0)', 'No. of pages must be positive')
]

向模型添加计算字段

有时,我们的字段需要通过计算获取值或从相同记录或关联记录中的值获取值。一个典型的示例是总额,由单价乘以数量计算所得。在Odoo模型中,可使用计算字段来实现。

为显示计算字段如何运作,我们将向图书模型中添加一个字段来计算图书发行日之后的天数。

也可以让计算字段可编辑和可搜索。我们也会在示例中进行实现。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

我们将编辑models/library_book.py代码文件来新增一个字段及支持它的逻辑的方法:

  1. 首先向图书模型添加一个新字段:
class LibraryBook(models.Model):
         # ...
         age_days = fields.Float(
             string='Days Since Release',
             compute='_compute_age',
             inverse='_inverse_age',
             search='_search_age',
             store=False, # optional
             compute_sudo=False # optional
         )
  1. 然后,添加一个值计算逻辑的方法:
# ...
    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.filtered('date_release'):
                delta = today - book.date_release
                book.age_days = delta.days
  1. 要添加方法及实现客入计算字段的逻辑,使用如下代码:
from 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
  1. 使用如下代码实现允许你在计算字段中进行搜索的逻辑:
from datetime import timedelta
    class LibraryBook(models.Model):
        # ...
        def _search_age(self, operator, value):
            today = fields.Date.today()
            value_days = timedelta(days=value)
            value_date = today - value_days
            # convert the operator:
            # book with age > value have a date < value_date
            operator_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计算字段来设置图书发布日期成为可能。search是可选属性,如果不想让该计算字段可编辑,可以忽略它。

也可以通过将search属性设置为方法名(类似compute和inverse)来让非存储的计算字段可搜索。类似inverse,search也是可选属性,如果不想让该计算字段可搜索,可以忽略它。

但是,这个方法预期不实现实际的搜索。而是接收用于搜索该字段的运算符和值来作为参数,并预期返回一个带有用于替换搜索条件的域。在我们的示例中,我们将一个Days Since Release 字段的搜索转换为Release Date字段上等价的搜索条件。

可选的store=True标记存储数据库中的字段。在这种情况下,执行计算后字段值会存储在数据库中,此后它们会像普通字段一样进行获取,而不是运行时重新计算。借助@api.depends装饰器,ORM会知道何时需要重新计算并更新这些存储值。你可以把它看作一个持久缓存。它还具有可将该字段作为搜索条件的好处,包含通过运算排序和分组,而无需实现search方法。

compute_sudo=True标记用于需要提权来执行计算的情况。这种情况可能是计算时需要使用终端用户无法访问的数据。

ℹ️使用它时需要小心,因为它会跳过权限规则 ,包含多公司设置的按公司分隔的规则。确保反复确认在计算中所使用的域来避免相关的问题。

暴露存储在其它模型中的关联字段

在从服务端读取数据时,Odoo客户端仅能获取模型中存在的字段及查询的值。客户端代码不同于服务端,无法使用点号标记来获取关联表中的数据。

但是,这些字段可通过将它们添加为关联字段来进行访问。我们将会让图书模型中的出版社城市可以被访问。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

编辑models/library_book.py文件添加一个新关联字段:

  1. 确保我们有一个图书出版社的字段:
class LibraryBook(models.Model):
         # ...
         publisher_id = fields.Many2one(
             'res.partner', string='Publisher')
  1. 接着,为出版社城市添加一个关联字段:
# 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中这通过使用引用字段来实现。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

编辑models/library_book.py文件来添加新的关联字段:

  1. 首先,我们需要添加一个帮助方法来运行构建一个可选目标模型列表:
from odoo import models, fields, api
    class LibraryBook(models.Model):
         # ...
         @api.model
         def _referencable_models(self):
             models = self.env['ir.model'].search([
                 ('field_id.name', '=', 'message_ids')])
             return [(x.model, x.name) for x in models]
  1. 然后,我们需要添加Reference字段来使用上述的函数提供一个可选模型列表:
ref_doc_id = fields.Reference(
        selection='_referencable_models',
        string='Reference Document')

因为我们修改了模型的结构,需要升级模块来启动这些修改。

运行原理...

引用字段类似于many-to-one字段,不同的是它们允许用户选择要关联的模型。

目标模型可通过由selection属性提供的列表进行选择。selection属性应是一个包含两个元素的元组,第一个元素是模型的内部标识符,第二个是它的文件描述。

以下是一个示例:

[('res.users', 'User'), ('res.partner', 'Partner')]

但是,不需要提供一个固定的列表,我们可以使用最通用的模型。为进行简化,我们使用带有消息功能的所有模型。使用_referencable_models方法,我们动态地提供了一个模型列表。

本节一开始提供了一个函数来浏览所有模型记录,可供动态引用来创建用于提供给selection属性的列表。虽然两种形式都允许,我们在引号内声明了函数名,而不是不加引号直接引用函数。这更为灵活,比如它允许所引用的函数可以在代码的后面进行定义,在使用直接引用时则不能这么做。

该函数需要一个@api.model装饰器,因为它在模型级别而非记录集级别上进行操作。

ℹ️虽然这个功能看起来很棒,它运行的开销会很大。使用引用字段显示大量记录(如在列表视图中)会带来很重的数据库负载,因为每个值都需在一个单独的查询中进行查找。它也不能像常规关联字段那样利用数据库的引用一致性。

使用继承向模型添加功能

Odoo一个最重要的功能是模块插件可以继承其它模块插件中定义的功能,而无需编辑原功能中的代码。这可以是添加字段或方法,修改已有字段或继承已有方法来执行额外的逻辑。

这是继承中最常使用的方法,在官方文档中称之为传统继承或经典继承。

我们将继承内置的Partner模型来添加所著书数量的计算字段。这包含对已有模型添加一个字段或一个方法。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

我们将继承内置的Partner模型。应在其自身的Python代码文件中实现,但为了简化讲解,我们将复用 models/library_book.py代码文件:

  1. 首先,我们将确保在Partner模型中有authored_book_ids反向关联并添加该计算字段:
class 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' )
  1. 然后,添加需要用于计算图书数量的方法:
# ...
    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类属性来实现。以下是一个示例:

class LibraryMember(models.Model):
     _inherit = 'res.partner'
     _name = 'library.member'

新模型有其自己的数据表,包含完全独立于res.partner父模型的自身数据。因其仍继承Partner模型,此后的任意修改也会影响到新模型。

在官方文档中,这被称为原型继承,但在实践中鲜有使用。原因在于代理继承通常可以更高效的方式满足了这一需求,也无需复制数据结构。参见本章中的使用代理继承将功能拷贝至另一个模型一节了解更多内容。

为可复用模型功能使用抽象模型

有时,会有一个具体的功能,我们想要添加到几个不同的模型中。在不同的文件中重复相同代码基本上是一种不良编程实践,最好可以一次实现多次复用。

抽象模型让我们可以创建一个通用模型来实现一些功能,然后由普通模型进行继承以使用该功能。

作为示例,我们将实现一个简单的存档功能。它将active字段加入到模型中(如果尚未存在)并添加一个存储方法来切换active标记。这可以生效是因为active是一个魔法字段,如果默认在模型中出现,active=False 的记录会在查询中被过滤掉。

下面我们将在图书模型中添加它。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

存储功能显然可独立为一个插件模块或者至少应有自己的Python代码文件。但为保持讲解尽可能简单,我们将会把它塞到models/library_book.py文件中:

  1. 为存档功能添加抽象模型。应在使用它的图书模型中定义:
class BaseArchive(models.AbstractModel):
         _name = 'base.archive'
         active = fields.Boolean(default=True)
         def do_archive(self):
             for record in self:
                record.active = not record.active
  1. 接着,我们将编辑图书模型来继承存档模型:
class 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对于称之为向导的更为复杂的用户交互会更为有用。该向导用于从用户请求输入。在第九章 高级服务端开发技巧中,我们探讨如何使用它们来实现高级用户交互。

使用代理继承将功能拷贝至另一个模型

传统继承使用_inherit执行原位修改来继承模型的功能。

但是有一些情况下,我们不想修改已有模型,而是基于已有模型新建一个模型来使用其已有的功能。这借由Odoo的代理继承实现,使用_inherits模型属性(注意这里多一个 s)。

传统继承与面向对象编程的概念有很大不同。代理继承则与其相似,其中可创建一个新的模型来包含父级模型中的功能。它还支持多态继承,这时从两个或多个其它的模型中进行继承。

我们的图书馆中有多本书籍。是时候修改让图书馆拥有会员了。对于图书会员,我们需要Partner模型中的所有身份和地址数据,也会想要保留一些有关会员的信息:起始日期、结束日期和会员卡号。

向Partner模型添加这些字段不是最好的方案,因为对于非会员的成员们无需使用到这些。使用一个带有额外字段的新模型继承Partner模型则会非常好。

准备工作

我们将复用第四章 创建Odoo插件模块中的my_library插件模块。

如何操作...

新图书会员模型应有自己的独立Python代码文件,但为保持讲解尽可能简单,我们将复用models/library_book.py文件:

  1. 添加新模型继承res.partner:
class LibraryMember(models.Model):
         _name = 'library.member'
         _inherits = {'res.partner': 'partner_id'}
         partner_id = fields.Many2one(
             'res.partner',
             ondelete='cascade')
  1. 接下来,我们将添加针对图书会员的字段:
# 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字典的创建:

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的传统继承会将功能拷贝到新模型中,虽然效率并不高。这在使用继承向模型添加功能一节中进行了讨论。

你可能感兴趣的:(Odoo 12开发者指南第五章 应用模型)