对象关系映射API

型号

模型字段被定义为模型本身的属性::

from odoo import models, fields
class AModel(models.Model):
    _name = 'a.model.name'

    field1 = fields.Char()

警告

这意味着您不能定义同名的字段和方法,最后一个将默默地覆盖前一个。

默认情况下,字段的标签(用户可见名称)是字段名称的大写版本,可以使用“string”参数覆盖。

field2 = fields.Integer(string="Field Label")

有关字段类型和参数的列表,请参阅 the fields reference

默认值定义为字段的参数,可以是值:

name = fields.Char(default="a value")

或作为调用来计算默认值的函数,该函数应返回该值:

def _default_name(self):
    return self.get_value()

name = fields.Char(default=lambda self: self._default_name())

应用程序编程接口

抽象模型

模型

瞬态模型

领域

基本领域

高级领域

日期(时间)字段

DatesDatetimes 在任何类型的业务应用程序中都是非常重要的字段。它们的滥用可能会产生看不见但痛苦的错误,本节旨在为 Odoo 开发人员提供避免滥用这些字段所需的知识。

将值分配给日期/日期时间字段时,以下选项有效:

  • datedatetime 对象。

  • 正确服务器格式的字符串:

    • YYYY-MM-DD 表示 Date 字段,

    • YYYY-MM-DD HH:MM:SS 用于 Datetime 字段。

  • FalseNone

Date 和 Datetime 字段类具有帮助器方法来尝试转换为兼容类型:

Example

解析来自外部源的日期/日期时间::

fields.Date.to_date(self._context.get('date_from'))

日期/日期时间比较最佳实践:

  • 日期字段**只能**与日期对象进行比较。

  • 日期时间字段**只能**与日期时间对象进行比较。

警告

表示日期和日期时间的字符串可以相互比较,但结果可能不是预期的结果,因为日期时间字符串始终大于日期字符串,因此**强烈**不鼓励这种做法。

日期和日期时间的常见操作(例如加法、减法或获取周期的开始/结束)通过 DateDatetime 公开。这些助手也可以通过导入 odoo.tools.date_utils 来使用。

注解

时区

日期时间字段在数据库中存储为 timestamp without timezone 列,并存储在 UTC 时区中。这是设计使然,因为它使 Odoo 数据库独立于托管服务器系统的时区。时区转换完全由客户端管理。

关系字段

伪关系字段

计算字段

可以使用“compute”参数计算字段(而不是直接从数据库读取)。 它必须将计算值分配给字段。如果它使用其他*字段*的值,则应使用 depends() 指定这些字段。

from odoo import api
total = fields.Float(compute='_compute_total')

@api.depends('value', 'tax')
def _compute_total(self):
    for record in self:
        record.total = record.value + record.value * record.tax
  • 使用子字段时,依赖关系可以是点路径:

    @api.depends('line_ids.value')
    def _compute_total(self):
        for record in self:
            record.total = sum(line.value for line in record.line_ids)
    
  • 默认情况下不存储计算字段,而是在请求时计算并返回它们。设置 store=True will store them in the database and automatically enable searching and grouping. Note that by default, compute_sudo=True 在字段上设置。

  • 还可以通过设置“search”参数来启用计算字段搜索。该值是返回 搜索域 的方法名称。

    upper_name = field.Char(compute='_compute_upper', search='_search_upper')
    
    def _search_upper(self, operator, value):
        if operator == 'like':
            operator = 'ilike'
        return Domain('name', operator, value)
    
  • 计算字段默认是只读的。要允许在计算字段上*设置*值,请使用“inverse”参数。它是反转计算并设置相关字段的函数的名称:

    document = fields.Char(compute='_get_document', inverse='_set_document')
    
    def _get_document(self):
        for record in self:
            with open(record.get_document_path) as f:
                record.document = f.read()
    def _set_document(self):
        for record in self:
            if not record.document: continue
            with open(record.get_document_path()) as f:
                f.write(record.document)
    
  • 可以通过相同的方法同时计算多个字段,只需对所有字段使用相同的方法并设置所有字段即可:

    discount_value = fields.Float(compute='_apply_discount')
    total = fields.Float(compute='_apply_discount')
    
    @api.depends('value', 'discount')
    def _apply_discount(self):
        for record in self:
            # compute actual discount from discount percentage
            discount = record.value * record.discount
            record.discount_value = discount
            record.total = record.value - discount
    

警告

虽然可以对多个字段使用相同的计算方法,但不建议对逆方法执行相同的操作。

在逆计算期间,使用所述逆的**所有**字段都受到保护,这意味着它们无法被计算,即使它们的值不在缓存中。

如果访问这些字段中的任何一个并且其值不在缓存中,则 ORM 将简单地为这些字段返回默认值 False 。这意味着逆字段的值(触发逆方法的字段除外)可能不会给出正确的值,这可能会破坏逆方法的预期行为。

自动字段

Model.id

标识符 field

如果当前记录集的长度为1,则返回其中唯一记录的id。

否则引发错误。

Model.display_name

Web 客户端中默认显示的名称 field

默认情况下,它等于 _rec_name 值字段,但可以通过覆盖 _compute_display_name 来自定义行为

访问日志字段

如果启用 _log_access,这些字段会自动设置和更新。可以禁用它以避免在表上创建或更新那些无用的字段。

默认情况下,_log_access 设置为与 _auto 相同的值

Model.create_date

存储记录创建时间,Datetime

Model.create_uid

who 创建记录 Many2one 存储到 res.users

Model.write_date

存储记录上次更新的时间,Datetime

Model.write_uid

将上次更新记录 Many2one 存储为“res.users”。

警告

_log_access *必须*在 TransientModel 上启用。

保留字段名称

一些字段名称是为自动化字段之外的预定义行为保留的。当需要相关行为时,应该在模型上定义它们:

Model.name

_rec_name 的默认值,用于在需要代表性“命名”的上下文中显示记录。

Char

Model.active

切换记录的全局可见性,如果“active` is set to ``False`”记录在大多数搜索和列表中不可见。

Boolean

特殊方法:

Model.state

对象的生命周期阶段,由 fields 上的“states”属性使用。

Selection

Model.parent_id

default_value 为 _parent_name,用于以树结构组织记录,并在域中启用“child_of` and ``parent_of`”运算符。

Many2one

Model.parent_path

_parent_store 设置为 True 时,用于存储反映 _parent_name 树结构的值,并优化运算符 child_of and parent_of in 搜索域. It must be declared with index=True 以便正确运行。

Char

Model.company_id

用于 Odoo 多公司行为的主字段名称。

:meth:~odoo.models._check_company 用于检查多公司一致性。定义记录是在公司之间共享(无值)还是只能由给定公司的用户访问。

Many2one :类型:res_company

约束和指标

与字段类似,您可以声明 ConstraintIndexUniqueIndex。属性的名称必须以 _ 开头,以避免与字段名称发生名称冲突。

您可以自定义错误消息。它们可以是字符串,并且它们的翻译将在内部反射约束表中提供。否则,它们可以是采用 (env, diag) 作为参数的函数,分别表示环境和 psycopg 诊断。

Example

class AModel(models.Model):
    _name = 'a.model'
    _my_check = models.Constraint("CHECK (x > y)", "x > y is not true")
    _name_idx = models.Index("(last_name, first_name)")

记录集

与模型和记录的交互是通过记录集(同一模型的记录的有序集合)执行的。

警告

与名称所暗示的相反,当前记录集可能包含重复项。这在未来可能会改变。

在模型上定义的方法在记录集上执行,并且它们的``self``是一个记录集:

class AModel(models.Model):
    _name = 'a.model'
    def a_method(self):
        # self can be anything between 0 records and all records in the
        # database
        self.do_operation()

迭代记录集将产生新的*单个记录*(“单例”)集,就像迭代 Python 字符串产生单个字符的字符串一样:

def do_operation(self):
    print(self) # => a.model(1, 2, 3, 4, 5)
    for record in self:
        print(record) # => a.model(1), then a.model(2), then a.model(3), ...

现场访问

记录集提供“活动记录”接口:模型字段可以作为属性直接从记录中读取和写入。

注解

当访问可能包含多个记录的记录集上的非关系字段时,请使用 mapped():

total_qty = sum(self.mapped('qty'))

字段值也可以像字典项一样访问,这比动态字段名称的“getattr()”更优雅、更安全。设置字段的值会触发数据库更新:

>>> record.name
Example Name
>>> record.company_id.name
Company Name
>>> record.name = "Bob"
>>> field = "name"
>>> record[field]
Bob

警告

尝试读取多个记录上的字段将引发非关系字段的错误。

访问关系字段(Many2oneOne2manyMany2many)*始终*返回一个记录集,如果未设置该字段,则返回空。

记录缓存和预取

Odoo 为记录的字段维护一个缓存,因此并不是每个字段访问都会发出数据库请求,这对性能来说是很糟糕的。以下示例仅查询数据库中的第一条语句:

record.name             # first access reads value from database
record.name             # second access gets value from cache

为了避免一次读取一条记录上的一个字段,Odoo 按照一些启发式“预取”记录和字段以获得良好的性能。一旦必须读取给定记录上的字段,ORM 实际上会读取更大记录集上的该字段,并将返回值存储在缓存中以供以后使用。预取记录集通常是通过迭代产生记录的记录集。此外,所有简单存储字段(布尔、整数、浮点数、字符、文本、日期、日期时间、选择、many2one)都会被一起获取;它们对应于模型表的列,并在同一查询中有效地获取。

考虑以下示例,其中“partners”是包含 1000 条记录的记录集。如果没有预取,循环将对数据库进行 2000 次查询。通过预取,仅进行一次查询::

for partner in partners:
    print partner.name          # first pass prefetches 'name' and 'lang'
                                # (and other fields) on all 'partners'
    print partner.lang

预取也适用于*辅助记录*:当读取关系字段时,它们的值(即记录)将被订阅以供将来预取。访问这些辅助记录之一会预取同一模型中的所有辅助记录。这使得以下示例仅生成两个查询,一个针对合作伙伴,一个针对国家/地区::

countries = set()
for partner in partners:
    country = partner.country_id        # first pass prefetches all partners
    countries.add(country.name)         # first pass prefetches all countries

其他资料

方法 search_fetch()fetch() 可用于填充记录缓存,通常是在预取机制无法正常工作的情况下。

方法装饰器

环境

>>> records.env
<Environment object ...>
>>> records.env.uid
3
>>> records.env.user
res.user(3)
>>> records.env.cr
<Cursor object ...>

当从其他记录集创建记录集时,环境会被继承。该环境可用于获取其他模型中的空记录集,并查询该模型:

>>> self.env['res.partner']
res.partner()
>>> self.env['res.partner'].search([('is_company', '=', True), ('customer', '=', True)])
res.partner(7, 18, 12, 14, 17, 19, 8, 31, 26, 16, 13, 20, 30, 22, 29, 15, 23, 28, 74)

一些惰性属性可用于访问环境(上下文)数据:

有用的环境方法

改变环境

SQL执行

环境中的 cr 属性是当前数据库事务的游标,允许直接执行 SQL,无论是对于难以使用 ORM 表达的查询(例如复杂的连接)还是出于性能原因::

self.env.cr.execute("some_sql", params)

警告

执行原始 SQL 会绕过 ORM,从而绕过 Odoo 安全规则。请确保在使用用户输入时对查询进行清理,如果您并不真正需要使用 SQL 查询,则更喜欢使用 ORM 实用程序。

构建 SQL 查询的推荐方法是使用包装对象

关于模型需要了解的一件重要的事情是它们不一定立即执行数据库更新。事实上,出于性能原因,框架在修改记录后延迟了字段的重新计算。一些数据库更新也被延迟。因此,在查询数据库之前,必须确保它包含查询的相关数据。此操作称为“刷新”并执行预期的数据库更新。

Example

# make sure that 'partner_id' is up-to-date in database
self.env['model'].flush_model(['partner_id'])

self.env.cr.execute(SQL("SELECT id FROM model WHERE partner_id IN %s", ids))
ids = [row[0] for row in self.env.cr.fetchall()]

在每个 SQL 查询之前,必须刷新该查询所需的数据。刷新分为三个级别,每个级别都有自己的 API。人们可以刷新所有内容、模型的所有记录或某些特定记录。因为延迟更新总体上可以提高性能,所以我们建议在刷新时“具体”。

由于模型使用相同的游标,并且 Environment 保存各种缓存,因此当在原始 SQL 中*更改*数据库时,这些缓存必须失效,否则模型的进一步使用可能会变得不连贯。使用``CREATE``, UPDATE or DELETE in SQL, but not ``SELECT``(只是读取数据库)时需要清除缓存。

Example

# make sure 'state' is up-to-date in database
self.env['model'].flush_model(['state'])

self.env.cr.execute("UPDATE model SET state=%s WHERE state=%s", ['new', 'old'])

# invalidate 'state' from the cache
self.env['model'].invalidate_model(['state'])

就像刷新一样,可以使整个缓存、模型的所有记录的缓存或特定记录的缓存失效。人们甚至可以使模型的某些记录或所有记录上的特定字段无效。由于缓存总体上提高了性能,因此我们建议在失效时“特定”。

上述方法使缓存和数据库保持一致。然而,如果计算字段依赖关系在数据库中被修改,则必须通知模型重新计算计算字段。框架唯一需要知道的是*哪些*记录上的*哪些*字段发生了变化。

Example

# make sure 'state' is up-to-date in database
self.env['model'].flush_model(['state'])

# use the RETURNING clause to retrieve which rows have changed
self.env.cr.execute("UPDATE model SET state=%s WHERE state=%s RETURNING id", ['new', 'old'])
ids = [row[0] for row in self.env.cr.fetchall()]

# invalidate the cache, and notify the update to the framework
records = self.env['model'].browse(ids)
records.invalidate_recordset(['state'])
records.modified(['state'])

人们必须弄清楚哪些记录已被修改。有很多方法可以做到这一点,可能涉及额外的 SQL 查询。在上面的示例中,我们利用已更新字段的修改记录上的“RETURNING` clause of PostgreSQL to retrieve the information without an extra query. After making the cache consistent by invalidation, invoke the method ``modified`”。

常见的ORM方法

创建/更新

搜索/阅读

领域

搜索域

搜索域是用于过滤和搜索记录集的一阶逻辑谓词。您可以将字段表达式上的简单条件与逻辑运算符结合起来。

Domain 可用作域的构建器。

# simple condition domains
d1 = Domain('name', '=', 'abc')
d2 = Domain('phone', 'like', '7620')

# combine domains
d3 = d1 & d2  # and
d4 = d1 | d2  # or
d5 = ~d1      # not

# combine and parse multiple domains (any iterable of domains)
Domain.AND([d1, d2, d3, ...])
Domain.OR([d4, d5, ...])

# constants
Domain.TRUE   # true domain
Domain.FALSE  # false domain

域可以是一个简单的条件“(field_expr, operator, value)”,其中:

  • field_expr (str)

    当前模型的字段名称,或使用点符号通过 Many2one 进行关系遍历,例如``’street’`` or 'partner_id.country'. If the field is a date(time) field, you can also specify a part of the date using 'field_name.granularity'. The supported granularities are 'year_number', 'quarter_number', 'month_number', 'iso_week_number'` `, ``'day_of_week', 'day_of_month', 'day_of_year', 'hour_number', 'minute_number', 'second_number'。它们都使用整数作为值。

  • operator (str)

    用于比较“field_expr` with the ``value`”的运算符。有效的运算符有:

    =

    等于

    !=

    不等于

    >

    大于

    >=

    大于或等于

    <

    少于

    <=

    小于或等于

    =?

    未设置或等于(如果``value`` is either None or False, otherwise behaves like ``=``则返回 true)

    =like (and not =like)

    匹配 field_expr against the value pattern. An underscore _ in the pattern stands for (matches) any single character; a percent sign % 匹配零个或多个字符的任何字符串。

    like (and not like)

    在匹配之前将 field_expr against the %value% pattern. Similar to =like but wraps value 与 ‘%’ 进行匹配

    ilike (and not ilike)

    不区分大小写``like``

    =ilike (and not =ilike)

    不区分大小写``=like``

    in (and not in)

    等于“value`, ``value`”中的任何项目应该是项目的集合

    child_of

    是“value”记录的子项(后代)(值可以是一项或一项列表)。

    考虑模型的语义(即遵循 _parent_name 命名的关系字段)。

    parent_of

    value 记录的父记录(上行记录)(值可以是一项或一系列项)。

    考虑模型的语义(即遵循 _parent_name 命名的关系字段)。

    any (and not any)

    如果通过 field_expr (Many2one, One2many, or Many2many) satisfies the provided domain value. The field_expr 的关系遍历中的任何记录应该是字段名称,则匹配。

    any! (and not any!)

    类似于“any”,但绕过访问检查。

  • value

    变量类型,必须与命名字段可比较(通过“operator”)。

Example

搜索名为*ABC*、电话号码或手机号码包含*7620*的合作伙伴:

Domain('name', '=', 'ABC') & (
  Domain('phone', 'ilike', '7620') | Domain('mobile', 'ilike', '7620')
)

要搜索销售订单以开具至少有一行缺货产品的发票:

Domain('invoice_status', '=', 'to invoice') \
  & Domain('order_line', 'any', Domain('product_id.qty_available', '<=', 0))

要搜索所有二月出生的伴侣::

Domain('birthday.month_number', '=', 2)

Domain 可用于将域序列化为“list` of simple conditions represented by 3-item tuple (or a list). Such a serialized form may be sometimes faster to read or write. Domain conditions can be combined using logical operators in a prefix notation. You can combine 2 domains using '&' (AND), '|' (OR) and you can negate 1 using ``’!’`”(NOT)。

# parse a domain (from list to Domain)
domain = Domain([('name', '=', 'abc'), ('phone', 'like', '7620')])

# serialize domain as a list (from Domain to list)
domain_list = list(domain)
# will output:
# ['&', ('name', '=', 'abc'), ('phone', 'like', '7620')]

动态时间值

在搜索域的上下文中,对于 date and datetime fields,该值可以是相对于用户时区*现在*的时刻。提供了一种简单的语言来指定这些日期。它是一个以空格分隔的术语字符串。第一个术语是可选的,是“今天”(午夜)或“现在”。然后,每个术语以“+”(加)、“-”(减)或“=”(集)开头,后跟整数和日期单位或小写工作日。

日期单位为:“d”(天)、“w”(周)、“m”(月)、“y”(年)、“H”(小时)、“M”(分钟)、“S”(秒)。对于工作日,“+”和“-”表示下一个和上一个工作日(除非我们已经在该工作日),“=”表示从星期一开始的本周。设置日期时,小单位(小时、分钟和秒)设置为 0。

Example

Domain('some_date', '<', 'now')  # now
Domain('some_date', '<', 'today')  # today at midnight
Domain('some_date', '<', '-3d +1H')  # now - 3 days + 1 hour
Domain('some_date', '<', '=3H')  # today at 3:00:00
Domain('some_date', '<', '=5d')  # 5th day of current month at midnight
Domain('some_date', '<', '=1m')  # January, same day of month at midnight
Domain('some_date', '>=', '=monday -1w')  # Monday of the previous week

记录(集)信息

odoo.models.env

返回给定记录集的环境。

类型

Environment

运营

记录集是不可变的,但可以使用各种集合操作组合相同模型的集合,返回新的记录集。

  • record in set returns whether record (which must be a 1-element recordset) is present in set. record not in set 是逆运算

  • set1 <= set2 and set1 < set2 return whether set1 is a subset of ``set2``(严格)

  • set1 >= set2 and set1 > set2 return whether set1 is a superset of ``set2``(严格)

  • set1 | set2 返回两个记录集的并集,一个包含任一源中存在的所有记录的新记录集

  • set1 & set2 返回两个记录集的交集,一个仅包含两个源中存在的记录的新记录集

  • set1 - set2 returns a new recordset containing only records of set1 which are not in set2

记录集是可迭代的,因此常用的 Python 工具可用于转换(map()sorted()ifilter() 等),但是它们返回 listiterator,从而消除了对其结果调用方法或使用集合操作的能力。

因此,记录集提供了以下返回记录集本身的操作(如果可能):

筛选

地图

注解

从 V13 开始,支持多关系字段访问,其工作方式类似于映射调用:

records.partner_id  # == records.mapped('partner_id')
records.partner_id.bank_ids  # == records.mapped('partner_id.bank_ids')
records.partner_id.mapped('name')  # == records.mapped('partner_id.name')

种类

分组

继承与延伸

Odoo 提供了三种不同的机制来以模块化方式扩展模型:

  • 从现有模型创建新模型,向副本添加新信息,但保持原始模块不变

  • 就地扩展其他模块中定义的模型,替换以前的版本

  • 将模型的某些字段委托给它包含的记录

../../../_images/inheritance_methods.png

经典传承

当同时使用 _inherit_name 属性时,Odoo 会使用现有模型(通过 _inherit 提供)作为基础创建一个新模型。新模型从其基础中获取所有字段、方法和元信息(默认值等)。

class Inheritance0(models.Model):
    _name = 'inheritance.0'
    _description = 'Inheritance Zero'

    name = fields.Char()

    def call(self):
        return self.check("model 0")

    def check(self, s):
        return "This is {} record {}".format(s, self.name)

class Inheritance1(models.Model):
    _name = 'inheritance.1'
    _inherit = ['inheritance.0']
    _description = 'Inheritance One'

    def call(self):
        return self.check("model 1")

并使用它们:

a = env['inheritance.0'].create({'name': 'A'})
b = env['inheritance.1'].create({'name': 'B'})

a.call()
b.call()

将产生:

“这是模型 0 记录 A” “这是模型 1 记录 B”

第二个模型继承了第一个模型的 check method and its name field, but overridden the call 方法,就像使用标准 Python inheritance 时一样。

扩大

当使用 _inherit 但省略 _name 时,新模型将替换现有模型,本质上是就地扩展它。这对于向现有模型(在其他模块中创建)添加新字段或方法,或者自定义或重新配置它们(例如更改其默认排序顺序)非常有用

class Extension0(models.Model):
    _name = 'extension.0'
    _description = 'Extension zero'

    name = fields.Char(default="A")

class Extension0(models.Model):
    _inherit = 'extension.0'

    description = fields.Char(default="Extended")
record = env['extension.0'].create({})
record.read()[0]

将产生:

{'name': "A", 'description': "Extended"}

警告

_inherit 设置为字符串时,_name 将设置为相同的值,除非显式设置 _name

注解

它还会产生各种 automatic fields ,除非它们已被禁用

代表团

第三种继承机制提供了更大的灵活性(可以在运行时更改)但功能更少:使用 _inherits 模型*委托*将当前模型上找不到的任何字段的查找委托给“子”模型。委托是通过在父模型上自动设置的 Reference 字段执行的。

主要区别在于含义。当使用Delegation时,模型**has one**而不是**is one**,将关系变成组合而不是继承

class Screen(models.Model):
    _name = 'delegation.screen'
    _description = 'Screen'

    size = fields.Float(string='Screen Size in inches')

class Keyboard(models.Model):
    _name = 'delegation.keyboard'
    _description = 'Keyboard'

    layout = fields.Char(string='Layout')

class Laptop(models.Model):
    _name = 'delegation.laptop'
    _description = 'Laptop'

    _inherits = {
        'delegation.screen': 'screen_id',
        'delegation.keyboard': 'keyboard_id',
    }

    name = fields.Char(string='Name')
    maker = fields.Char(string='Maker')

    # a Laptop has a screen
    screen_id = fields.Many2one('delegation.screen', required=True, ondelete="cascade")
    # a Laptop has a keyboard
    keyboard_id = fields.Many2one('delegation.keyboard', required=True, ondelete="cascade")
record = env['delegation.laptop'].create({
    'screen_id': env['delegation.screen'].create({'size': 13.0}).id,
    'keyboard_id': env['delegation.keyboard'].create({'layout': 'QWERTY'}).id,
})
record.size
record.layout

将导致:

13.0
'QWERTY'

并且可以直接在委托字段上写入:

record.write({'size': 14.0})

警告

当使用委托继承时,方法*不*被继承,只有字段被继承

警告

  • _inherits 或多或少已实现,如果可以的话避免它;

  • chained _inherits 本质上没有实现,我们不能保证最终行为的任何内容。

字段增量定义

字段被定义为模型类的类属性。如果扩展模型,还可以通过在子类上重新定义具有相同名称和相同类型的字段来扩展字段定义。在这种情况下,字段的属性取自父类,并由子类中给出的属性覆盖。

例如,下面的第二个类仅在字段“state”上添加工具提示

class FirstFoo(models.Model):
    state = fields.Selection([...], required=True)

class FirstFoo(models.Model):
    _inherit = ['first.foo']
    state = fields.Selection(help="Blah blah blah")

class WrongFirstFooClassName(models.Model):
    _name = 'first.foo'  # force the model name
    _inherit = ['first.foo']
    state = fields.Selection(help="Blah blah blah")

错误管理