编写可导入的模块

重要

本教程假设您熟悉 服务器框架 101 教程和 定义模块数据 教程。

尽管作为开发者,我们更倾向于使用 Python 的全部功能来编写模块,但在某些情况下可能无法做到这一点;例如在不允许部署自定义 Python 代码的托管解决方案中,如 Odoo.com 平台。

然而,Odoo 的灵活性旨在允许开箱即用的自定义。虽然通过 Studio 可以实现许多功能,但也可以在 XML 数据文件 中定义模型、字段和逻辑。这使得开发、维护和部署这些自定义更加容易。

在本教程中,我们将学习如何在 XML 数据文件中定义模型、字段和逻辑,并将它们打包成一个模块。这些模块有时被称为 可导入模块数据模块。我们还将了解这种模块开发方法的局限性。

问题陈述

像在 服务器框架 101 教程中一样,我们将继续探讨房地产相关概念。

我们的目标是创建一个新的应用程序,以类似于 服务器框架 101 教程的方式(尽管更简单)来管理房地产物业。我们将通过 XML 数据文件定义模型、字段和逻辑,而不是使用 Python 文件。

在本教程结束时,我们将在我们的应用中实现以下功能:

  • 出售房地产物业的管理

  • 将这些属性发布到网站上

  • 在线从网站接受报价

  • 当房产售出时向买方开具发票

模块结构

就像在任何开发项目中一样,清晰的结构有助于更轻松地管理和维护代码。

与标准 Odoo 模块使用 Python 和 XML 文件不同,数据模块仅使用 XML 文件。因此,预期你的工作树将看起来如下所示:

estate
├── actions
│   └── *.xml
├── models
│   └── *.xml
├── security
│   └── ir.model.access.csv
│   └── estate_security.xml
├── views
│   └── *.xml
├── __init__.py
└── __manifest__.py

您将拥有的只有 __init__.py__manifest__.py 文件。__manifest__.py 文件将与任何 Odoo 模块的相同,但还会在其 data 列表中导入其模型。

记得在 __manifest__.pydata 部分中按依赖顺序列出文件,通常从模型文件开始。

__init__.py 文件是空的,但如果将来您希望通过传统方式(通过将模块添加到 addons 路径中)部署您的模块,则必须存在。对于将要*导入*的模块来说,它并不是严格必需的,但保留它是一个良好的实践。

部署模块

要部署该模块,您需要创建一个模块的 zip 文件并将其上传到您的 Odoo 实例。确保您的实例上已安装模块 base_import_module,然后转到 应用 ‣ 导入模块 并上传 zip 文件。您必须处于 开发者模式 才能看到 导入模块 菜单项。

如果您修改了模块,需要重新创建一个 zip 文件并再次上传,这将重新加载模块中的所有数据。但是请注意,某些操作是不可行的,例如更改您之前创建的字段类型。由模块旧版本创建的数据(如已删除的字段)不会被自动删除。一般来说,处理这种情况最简单的方法是使用一个新的数据库,或者在上传新版本之前先卸载该模块。

上传模块时,向导将接受两种选项:

  • 强制初始化: 如果您的模块已经安装,再次上传时;勾选此选项将强制更新 XML 文件中标记为 noupdate="1" 的所有数据。

  • 导入演示数据: 自明

也可以使用 odoo-bin 命令行工具通过 deploy 命令部署模块:

$ odoo-bin deploy <path_to_your_module> https://<your_odoo_instance> --login <your_login> --password <your_password>

此命令还接受 --force 选项,该选项等同于向导中的 强制初始化 选项。

注意,用于部署模块的用户必须具有 Administration/Settings 访问权限。

Exercise

  1. 创建以下文件夹和文件:

    • /home/$USER/src/tutorials/estate/__init__.py

    • /home/$USER/src/tutorials/estate/__manifest__.py

    我们的 __manifest__.py 文件应仅定义模块的名称和依赖项。目前,唯一必需的框架模块是 base``(以及 ``base_import_module - 虽然您的模块在严格意义上并不依赖它,但您需要它才能导入您的模块)。

  2. 创建您的模块的 ZIP 文件并将其上传到您的 Odoo 实例。

模型和基本字段

正如你所想象的,在 XML 文件中定义模型和字段不如在 Python 中直接明了。

由于数据文件是按顺序读取的,因此您必须按照正确的顺序定义元素。例如,您必须在定义该模型上的字段之前先定义模型,并且在将字段添加到视图之前必须先定义字段。

此外,XML 比 Python 要冗长得多。

让我们从在模块的 models 目录中定义一个简单的模型开始,用于表示房地产资产。

Odoo 模型在数据库中以 ir.model 记录的形式存储。与其他任何记录一样,它们可以在 XML 文件中定义:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="model_real_estate_property" model="ir.model">
        <field name="name">Real Estate Property</field>
        <field name="model">x_estate.property</field>
    </record>
</odoo>

请注意,数据文件中定义的所有模型和字段必须以 x_ 作为前缀;这是强制性的,用于将它们与 Python 文件中定义的模型和字段区分开来。

与在 Python 中定义的经典模型类似,Odoo 会自动为模型添加多个字段:

  • id (Id) 模型记录的唯一标识符。

  • create_date (Datetime) 记录的创建日期。

  • create_uid (Many2one) 创建该记录的用户。

  • write_date (Datetime) 记录的最后修改日期。

  • write_uid (Many2one) 最后修改该记录的用户。

我们还可以向我们的新模型添加多个字段。让我们添加一些简单的字段,比如名称(字符串)、销售价格(浮点数)、描述(作为 HTML)和邮编(作为字符)。

与模型类似,字段只是 ir.model.fields 模型的记录,可以在数据文件中以这种方式进行定义:

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_name" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_name</field>
        <field name="field_description">Name</field>
        <field name="ttype">char</field>
        <field name="required">True</field>
    </record>

    <record id="field_real_estate_property_selling_price" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_selling_price</field>
        <field name="field_description">Selling Price</field>
        <field name="ttype">float</field>
        <field name="required">True</field>
    </record>

    <record id="field_real_estate_property_description" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_description</field>
        <field name="field_description">Description</field>
        <field name="ttype">html</field>
    </record>

    <record id="field_real_estate_property_postcode" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_postcode</field>
        <field name="field_description">Postcode</field>
        <field name="ttype">char</field>
    </record>
</odoo>

你可以为你的新字段设置各种属性。对于基本字段,包括以下内容:

  • name: 字段的技术名称(必须以 x_ 开头)

  • field_description: 字段的标签

  • help: 字段的帮助文本,在界面中显示

  • ttype:字段的类型(例如 charintegerfloathtml 等)

  • 必填: 字段是否为必填项(默认:False

  • 只读: 字段是否为只读(默认:False

  • index: 该字段是否被索引(默认: False

  • copied: 该字段在复制记录时是否被复制(默认:非关系型且非计算字段为 True,关系型和计算字段为 False

  • translate: 该字段是否可翻译(默认: False

属性还可用于控制 HTML 清洗以及其他更高级的功能;有关完整列表,请参阅数据库中的 ir.model.fields 模型,该模型位于 设置 ‣ 技术 ‣ 数据库结构 ‣ 字段 菜单中,或查看 base 模块中的 ir.model.fields 模型定义。

Exercise

向表中添加以下基本字段:

字段

类型

必填

可用日期

日期

x_expected_price

浮点数

正确

x_bedrooms

整数

x_living_area

整数

x_facades

整数

x_garage

布尔型

x_garden

布尔型

x_garden_area

整数

x_garden_orientation

选择

x_garden_orientation 字段必须有 4 个可能的值:’North’、’South’、’East’ 和 ‘West’。选择列表必须首先创建该字段本身的 ir.model.fields 记录,然后创建 ir.model.fields.selection 记录。这些记录包含三个字段:field_idname`(用户界面中的名称)和 `value`(数据库中的值)。还可以设置一个 `sequence 字段,用于控制用户界面中选择项的显示顺序(序列值较小的会优先显示)。

默认值

在 Python 中,可以使用字段声明中的 default 参数为字段设置默认值。在数据模块中,通过为每个字段创建一个 ir.default 记录来设置默认值。例如,可以通过创建以下记录,将 x_selling_price 字段的默认值设置为 100000,适用于所有资产:

<odoo>
    <!-- ...model definition from before... -->
    <record id="default_real_estate_property_selling_price" model="ir.default">
        <field name="field_id" ref="estate.field_real_estate_property_selling_price" />
        <field name="json_value">100000</field>
    </record>
</odoo>

如需了解更多详细信息,请参阅数据库中的 ir.default 模型,该模型位于 设置 ‣ 技术 ‣ 操作 ‣ 用户定义的默认值 菜单中,或查看 base 模块中 ir.default 模型的定义。

警告

这些默认值是静态的,但可以通过 ir.default 记录的 user_idcompany_id 字段由公司和/或用户进行设置。这意味着例如,无法为 x_date_availability 字段设置动态的默认值“今天”。

安全

数据模块中的安全机制与 Python 模块完全相同,相关内容请参见 Chapter 4: Security - A Brief Introduction

有关详细信息,请参阅该教程。

Exercise

  1. 在适当的文件夹中创建 ir.model.access.csv 文件,并在 __manifest__.py 文件中对其进行定义。

  2. 将读取、写入、创建和删除权限授予用户组 base.group_user

小技巧

日志中的警告消息已经为你提供了大部分解决方案 ;-)

视图

视图是允许用户与数据交互的用户界面组件。它们在 XML 文件中定义,可以在你的模块的 views 目录中找到。

由于视图和操作已经在 Chapter 5: Finally, Some UI To Play WithChapter 6: Basic Views 中定义,此处将不再详细说明。

Exercise

estate 模块添加一个基本的用户界面。

estate 模块中添加一个基本的用户界面,以允许用户查看、创建、编辑和删除房地产资产。

  • 为模型 x_estate.property 创建一个动作。

  • 为模型 x_estate.property 创建一个树形视图。

  • 为模型 x_estate.property 创建一个表单视图。

  • 将视图添加到动作中。

  • 在主菜单中添加一个菜单项,以允许用户访问该动作。

关系

关系型系统(如 Odoo)的真正强大之处在于能够将记录相互关联。在普通的 Python 模块中,可以通过在模型上定义新字段,用一行代码将其与其他模型关联。在数据模块中,这仍然是可能的,但由于不能使用与 Python 相同的语法,因此需要做一些额外的工作。

Chapter 7: Relations Between Models 中所述,我们将在 estate 模块中添加一些关联。我们将添加以下链接:

  • 购买该房产的客户

  • 出售该房产的房地产经纪人

  • 该属性类型:房屋、公寓、顶层公寓、城堡……

  • 一个用于描述该属性的标签列表:温馨、翻新过……

  • 一份已收到的报价列表

一对一

一个一对多关系是到另一个对象的简单链接。例如,为了定义对 res.partner 的链接,我们可以在我们的模型中定义一个新字段:

<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_partner_id" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_partner_id</field>
        <field name="field_description">Customer</field>
        <field name="ttype">many2one</field>
        <field name="relation">res.partner</field>
    </record>
</odoo>

在多对一字段的情况下,可以设置多个属性以详细说明关系:

  • relation: 要链接的模型名称(必填)

  • ondelete: 删除记录时执行的操作(默认:set null

  • domain: 用于对关系应用的筛选器

Exercise

  1. 创建一个新的模型 x_estate.property.type,包含以下字段:

    字段

    类型

    必填

    名称

    字符

    正确

  2. x_estate.property.type 模型添加一个动作、列表视图和菜单项。

  3. x_estate.property.type 模型添加访问权限,以授予用户访问权限。

  4. x_estate.property 模型上创建以下字段:

    字段

    类型

    必填

    x_属性类型_id

    许多对一(x_estate.property.type

    正确

    `x_partner_id`(买方)

    多对一(res.partner

    `x_user_id`(销售代表)

    许多对一(res.users

  5. x_estate.property 模型的表单视图中包含新字段。

多对多

一个多对多关系是指向一组对象的关系。在我们的示例中,我们将定义一个指向新模型 x_estate.property.tag 的多对多关系。此标签表示房产的一个特征,例如:翻新、温馨等。

一个属性可以有多个标签,而一个标签也可以分配给多个属性——这就是典型的多对多关系。

多对多字段的定义方式与多对一字段相同,但将 ttype 设置为 many2manyrelation 属性也设置为要链接的模型名称。还可以设置其他属性来控制关系:

  • relation_table: 用于关系的表名

  • column1column2:用于关系的列名

这些属性是可选的,通常仅在两个模型之间存在多个多对多字段以避免冲突时才需要指定;在大多数情况下,Odoo ORM 能够确定要使用的正确关系表和列。

Exercise

  1. 创建一个新的模型 x_estate.property.tag,包含以下字段:

    字段

    类型

    必填

    名称

    字符

    正确

  2. x_estate.property.tag 模型添加一个动作、列表视图和菜单项。

  3. x_estate.property.tag 模型添加访问权限,以允许用户访问。

  4. x_estate.property 模型上创建以下字段:

    字段

    类型

    x_属性标签_ids

    多对多(x_estate.property.tag

  5. x_estate.property 模型的表单视图中包含新字段。

一对多

一个一对多关系是指对一组对象的关联。在我们的示例中,我们将定义一个指向新模型 x_estate.property.offer 的一对多关系。该关联表示客户对购买房产提出的报价。

一对一字段的定义方式与多对一字段相同,但需将 ttype 设置为 one2manyrelation 属性也应设置为要链接的模型名称。还需要设置另一个属性来控制关系:

  • relation_field: 关联模型上包含对父模型引用的字段名称(多对一字段)。此字段用于将两个模型关联起来。

Exercise

  1. 创建一个新的模型 x_estate.property.offer,包含以下字段:

    字段

    类型

    必填

    x_price

    浮点数

    正确

    x_status

    选择

    已接受,已拒绝

    x_合作伙伴ID

    多对一(res.partner

    正确

    x_property_id

    多对一(x_estate.property

    正确

  2. x_estate.property.offer 模型添加访问权限,以允许用户访问。

  3. 创建一个树形视图和一个表单视图,包含价格、合作伙伴ID和状态字段。
    无需创建动作或菜单。
  4. 在你的 x_estate.property 模型及其表单视图中添加字段 x_offer_ids

计算字段

计算字段是 Odoo 的核心概念,用于定义基于其他字段计算得出的字段。这对于从其他字段派生出的字段非常有用,例如子记录的总和(如销售订单中所有商品价格的总和)。

引用:有关此主题的文档可在 计算字段 中找到。

数据模块可以定义任何类型的计算字段,但与 Python 模块相比功能非常有限。事实上,由于数据模块旨在部署在不允许运行任意代码的系统上,因此允许使用的 Python 代码非常有限。

注解

所有为数据模块编写的 Python 代码都在一个沙箱环境中执行,该环境限制了可以执行的操作。例如,您不能导入库,不能访问任何操作系统文件,甚至不能向控制台打印内容。提供了一些实用工具,但具体取决于所使用的沙箱环境类型。

在计算方法的情况下,沙箱非常有限,仅提供执行代码所需的最基本工具。除了 Python 内置函数外,您还可以访问 datetimedateutiltime 模块(例如,用于帮助进行日期计算)。

注意,沙箱中已禁用“点赋值”,因此您不能在计算方法中编写 property.x_total_area = 1。您必须使用字典访问方式:property['x_total_area'] = 1。对于字段 访问,点表示法可以正常工作:property.x_garden_area 将返回 x_garden_area 字段的值。

我们之前在 x_estate.property 模型上定义了两个“面积”字段:living_areagarden_area。要在模型上定义一个返回两个面积之和的计算字段,我们可以将以下代码添加到我们的数据模块中:

<odoo>
    <!-- ...model definition from before... -->
    <record id="field_real_estate_property_total_area" model="ir.model.fields">
        <field name="model_id" ref="estate.model_real_estate_property" />
        <field name="name">x_total_area</field>
        <field name="field_description">Total Area</field>
        <field name="ttype">float</field>
        <field name="depends">x_living_area,x_garden_area</field>
        <field name="compute"><![CDATA[
for property in self:
    property['x_total_area'] = property.x_living_area + property.x_garden_area
        ]]>
        </field>
    </record>
</odoo>

注解

在服务器操作中,你会遍历一个 records 变量,而在计算字段的情况下,你会遍历一个 self 变量,该变量包含正在计算该字段的记录集。

depends 属性用于定义计算字段所依赖的字段,compute 属性用于定义用于计算字段的代码(使用 Python 代码)。

与 Python 模块不同,计算字段默认是存储的。如果您希望计算字段不被存储(例如出于性能考虑或避免数据库膨胀),可以将 store 属性设置为 False

CDATA 区段用于告知 XML 解析器,该内容是一个字符串而非 XML;这可以防止解析器在模块安装时将 Python 代码当作 XML 进行解释,或在代码插入数据库时添加额外的空格等。

Exercise

  1. x_estate.property 模型中添加一个计算字段,该字段返回 x_living_areax_garden_area 字段的总和,如上所示。

  2. x_estate.property 模型的表单视图中包含该字段。

注解

与 Python 模块不同,无法为计算字段定义反向方法或搜索方法。

代码和业务逻辑

服务器操作

在一个 Python 模块中,你可以在你的模型上定义任何方法。一种常见的使用模式是向你的模型添加所谓的“操作”方法,然后将这些方法绑定到用户界面中的按钮(例如确认报价单、发布发票等)。

在数据模块中,您可以通过为您的模型定义 服务器操作 来实现相同的效果。服务器操作表示在服务器上动态执行的逻辑片段。这些操作可以通过 设置 ‣ 技术 ‣ 操作 ‣ 服务器操作 菜单直接在数据库中手动配置,并且可以有不同类型的;在我们的情况下,我们将使用 code 类型,它允许我们在沙盒环境中运行任何 Python 代码。

此环境包含多个实用工具,可帮助您与 Odoo 数据库进行交互:

  • self: 在其上执行动作的记录

  • env: 记录的环境

  • 模型: 记录的模型

  • 用户uid:当前用户及其 ID

  • datetimedateutiltimezonetime:用于帮助进行日期/时间计算的库

  • float_compare: 一个实用函数,用于根据给定的精度比较两个浮点值

  • b64encodeb64decode:用于将值以 base64 编码和解码的实用函数

  • 命令:一个用于帮助构建复杂表达式和命令的实用类(参见 ORM 引用 中的 Command 类)

此外,您还可以通过 recordrecords 变量访问执行该动作的记录集(通常在从表单视图执行动作时为单条记录,而在从列表视图执行动作时为多条记录)。

注解

如果您的动作需要向客户端返回一个动作(例如将用户重定向到另一个视图),您可以在服务器动作的代码中将它赋值给一个 action 变量。代码沙箱会在执行完您的代码后检查定义的变量,并在检测到 action 变量存在时自动返回该动作。

如果已安装 website 模块,则在代码沙箱中将可以使用 request 对象,并且可以将 response 对象赋值给 response 变量,以类似的方式向客户端返回响应。有关此内容的详细探讨,请参阅 网站控制器 章节。

例如,我们可以在 x_estate.property 模型上定义一个动作,将所有其报价的 x_status 字段设置为 拒绝

<record id="action_x_estate_property_refuse_all_offers" model="ir.actions.server">
    <field name="name">Refuse all offers</field>
    <field name="model_id" ref="estate.model_real_estate_property"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for property in records:
    property.x_offer_ids.write({'x_status': 'refused'})
    ]]></field>
</record>

要在 x_estate.property 模型的表单视图中将此动作作为按钮包含进来,我们可以在表单视图的头部添加以下 按钮 节点:

<!-- form view definition from your code... -->
<header>
    <button name="estate.action_x_estate_property_refuse_all_offers" type="action" string="Refuse all offers"/>
</header>

也可以在齿轮图标 () 中添加一个条目来运行此动作(例如,为了避免在已经拥挤的视图中添加按钮)。要做到这一点,您可以将服务器动作*绑定*到模型以及特定类型的视图:

<record id="action_x_estate_property_refuse_all_offers" model="ir.actions.server">
    <field name="name">Refuse all offers</field>
    <field name="model_id" ref="estate.model_real_estate_property"/>
    <field name="state">code</field>
    <field name="binding_model_id" ref="estate.model_real_estate_property"/>
    <field name="binding_view_types">tree,form</field>
    <field name="code"><![CDATA[
for property in records:
    property.x_offer_ids.write({'x_status': 'refused'})
    ]]></field>
</record>

这将使该动作在 x_estate.property 模型的齿轮图标 () 中可用,在列表视图中(当通过复选框选择一个或多个记录时)以及表单视图中。

Exercise

  1. x_estate.property.offer 模型中添加一个服务器动作,将报价的 x_status 字段设置为 Accepted,并相应地更新与该报价关联的房产的售价和买家。此动作还应将同一房产上的其他所有报价标记为 Refused

  2. 在报价的嵌入式列表视图中添加一个按钮,用于执行此操作

../../_images/offer_accept_button.png

覆盖 Python 模型

通过用户界面元素

与 Python 模块不同,无法干净地覆盖 Python 模型的方法。

然而,在某些情况下,可以替换调用这些方法的用户界面元素,并在服务器动作中拦截对这些方法的调用。

一个典型的例子是与 Odoo 的 Sales 应用进行集成。假设你的房地产模块与销售应用进行集成,当某个特定产品被售出时(例如,用于管理房产销售的报价单),你希望在你的模块中自动生成一个新的房产记录。

要实现这一点,你需要:

  • 创建一个服务器动作,调用按钮的原始方法,并在该方法调用之前或之后添加自定义逻辑

  • 将视图中的按钮替换为调用服务器动作的自定义按钮

<record id="view_sale_order_form" model="ir.ui.view">
    <field name="name">sale.order.form.inherit.estate</field>
    <field name="model">sale.order</field>
    <field name="inherit_id" ref="sale.view_order_form" />
    <field name="arch" type="xml">
        <xpath expr="//button[@name='action_confirm'][@type='object']" position="attributes">
            <attribute name="type">action</attribute>
            <attribute name="name">estate.action_x_estate_property_create_from_sale_order</attribute>
        </xpath>
        <!-- since the button is present twice in the original view, we need to replace it twice -->
        <xpath expr="//button[@name='action_confirm'][@type='object']" position="attributes">
            <attribute name="type">action</attribute>
            <attribute name="name">estate.action_x_estate_property_create_from_sale_order</attribute>
        </xpath>
    </field>
</record>

<record id="action_x_estate_property_create_from_sale_order" model="ir.actions.server">
    <field name="name">Confirm and create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for order in records:
    order.action_confirm()
    property_type = env['x_estate.property.type'].sudo().search([('x_name', '=', 'Other')], limit=1)
    property = env['x_estate.property'].sudo().create({
        'x_name': order.name,
        'x_expected_price': 0,
        'x_selling_price': 0,
        'x_sale_order_id': order.id,
        'x_property_type_id': property_type.id,
    })
    ]]></field>
</record>

通过自动化规则

自动化规则是一种根据特定触发条件(如状态变化、添加标签等)在数据库中的记录上自动执行操作的方式。它们可以用于将行为与记录的生命周期事件关联起来,例如在报价被接受时发送邮件。

使用自动化规则来扩展标准行为,比基于用户界面的方法更加稳健,因为即使生命周期事件是通过按钮以外的方式触发的(例如通过网络钩子或对方法的直接调用;比如当通过门户或电子商务确认报价时),自动化规则也会运行。然而,它们的设置要稍微复杂一些,需要确保通过设置特定字段进行监控等方式,使自动化仅在适当的时候运行。

文档:有关此主题的更完整文档,请参阅 Automation rules

注解

自动化规则不属于 base 模块;它们包含在 base_automation 模块中;因此,如果您在自己的数据模块中定义自动化规则,需要确保 base_automation 是您模块的依赖项。

安装完成后,自动化规则通过 设置 ‣ 技术 ‣ 自动化 ‣ 自动化规则 菜单进行管理。

自动化规则特别适用于将数据模块与现有的标准 Odoo 模块关联起来。由于数据模块无法覆盖方法,将自动化与标准模型的生命周期变化关联是扩展标准模块的常见方式。

如果我们要使用自动化规则重写上一节中的示例,需要进行一些更改:

  • 服务器动作不应再调用按钮的原始方法(相反,原始方法将触发更改,从而触发自动化规则)

  • 视图扩展不是必需的

  • 我们需要定义一个自动化规则,以在适当事件上触发服务器动作

<record id="action_x_estate_property_create_from_sale_order" model="ir.actions.server">
    <field name="name">Create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="state">code</field>
    <field name="code"><![CDATA[
for order in records:
    property_type = env['x_estate.property.type'].sudo().search([('x_name', '=', 'Other')], limit=1)
    property = env['x_estate.property'].sudo().create({
        'x_name': order.name,
        'x_expected_price': 0,
        'x_selling_price': 0,
        'x_sale_order_id': order.id,
        'x_property_type_id': property_type.id,
    })
    ]]></field>
</record>

<record id="automation_rule_x_estate_property_create_from_sale_order" model="base.automation">
    <field name="name">Create property from sale order</field>
    <field name="model_id" ref="sale.model_sale_order"/>
    <field name="trigger">on_state_set</field>
    <field name="trg_selection_field_id" ref="sale.selection__sale_order__state__sale"/>
    <field name="trigger_field_ids" eval="[(4, ref('sale.field_sale_order__state'))]"/>
    <field name="action_server_ids" eval="[(4, ref('estate.action_x_estate_property_create_from_sale_order'))]"/>
</record>

请注意,标准 Odoo 模型、字段、选择值等的 XML ID 可以在 Odoo 实例中通过导航到技术菜单中的相应记录,并使用调试菜单中的 查看元数据 菜单项来找到。模型的 XML ID 只是将模型名称中的点替换为下划线,并在前面加上 model_``(例如,``sale.model_sale_order 对应的是 sale 模块中定义的 sale.order);字段的 XML ID 是将模型名称中的点替换为下划线,并在前面加上 field_,然后加上模型名称和字段名称(例如,sale.field_sale_order__namesale 模块中定义的 sale.order 模型的 name 字段的 XML ID)。

网站控制器

Odoo 中的 HTTP 控制器通常在模块的 controllers 目录中定义。在数据模块中,如果已安装网站模块,则可以定义作为控制器行为的服务器操作。

当安装网站模块时,服务器动作可以标记为 Available on the website,并指定一个路径(完整路径始终以 /actions 作为前缀,以避免网址冲突);全局的 request 对象会在代码服务器动作的本地作用域中可用。

request 对象提供了几种方法来访问请求的正文:

  • request.get_http_params(): 从查询字符串和正文中的表单提取键值对(包括 application/x-www-form-urlencodedmultipart/form-data 类型)。

  • request.get_json_data(): 从请求正文提取 JSON 数据。

由于无法从服务器动作中返回值,为了定义要返回的响应,可以将一个类似响应的对象赋值给 response 变量,该变量将自动返回到网站。

以下是一个简单的网站控制器示例,当调用网址 /actions/estate 时,将返回一个房产列表:

<record id="server_action_estate_list" model="ir.actions.server">
    <field name="name">Estate List Controller</field>
    <field name="model_id" ref="estate.model_real_estate_property" />
    <field name="website_published">True</field>
    <field name="website_path">estate</field>
    <field name="state">code</field>
    <field name="code"><![CDATA[
html = '<html><body><h1>Properties</h1><ul>'
for property in request.env['x_estate.property'].search([]):
    html += f'<li>{property.x_name}</li>'
html += '</ul></body></html>'
response = request.make_response(html)
    ]]></field>
</record>

request 对象中提供了多种有用的方法,以帮助生成响应对象:

  • request.render(template, qcontext=None, lazy=True, **kw) 用于使用其 xmlid 渲染一个 QWeb 模板;额外的关键字参数会传递给 werkzeug.Response 对象(例如用于设置 cookies、headers 等)。

  • request.redirect(location, code=303, local=True) 用于重定向到另一个 URL;local 参数用于指定重定向是否应相对于网站(默认值:True)。

  • request.notfound() 用于向网站返回一个 werkzeug.HTTPException 异常,以表示 404 错误。

  • request.make_response(data, headers=None, cookies=None, status=200) 用于手动创建一个 werkzeug.Response 对象;status 参数是要返回的 HTTP 状态码(默认值:200)。

  • request.make_json_response(data, headers=None, cookies=None, status=200) 用于手动创建 JSON 响应;数据将使用 json.dumps 工具进行 JSON 序列化;这在通过接口调用设置服务器间通信时非常有用。

有关实现细节或其他(较少使用)方法,请参阅 odoo.http 模块中 Request 对象的实现。

请注意,安全相关的问题由开发者处理(通常通过安全规则或使用 sudo 访问记录)。

注解

该服务器动作的 model_id 字段所使用的模型必须对公开用户可访问,以便此服务器动作执行写操作;否则,服务器动作将返回 403 错误。一种避免授予访问权限的方法是将您的服务器动作链接到一个已对公开用户可访问的模型,一个典型的(尽管有些奇怪)例子是将服务器动作链接到 ir.filters 模型。

Exercise

在您的模块中添加一个 JSON 接口,以便外部服务可以获取待售房产列表。

  1. 向模型中添加一个新的 x_api_published 字段,用于控制属性是否在 API 上发布。

  2. 向访问权限记录中添加一项,以允许公开用户对模型进行读写操作

  3. 防止公开用户进行任何写操作,通过为写操作添加一个不可能的域规则(例如 [('id', '=', False)]

  4. 添加一条记录规则,以便公开用户可以读取标记为 x_api_published 的记录

  5. 在调用网址 /actions/api/estate 时,添加一个服务器动作,以 JSON 格式返回一组属性。

一点 JavaScript

虽然可导入的模块不能包含 Python 文件,但对 JavaScript 文件没有这样的限制。将 JavaScript 文件添加到您的可导入模块中,与将其添加到标准 Odoo 模块中的方式完全相同。

这意味着可导入的模块可以包含新的字段组件,甚至全新的视图。

例如,我们向 Estate 模块添加一个简单的“引导流程”。引导流程是 Odoo 中一种标准机制,用于通过引导用户使用您的应用程序来帮助他们上手。

可以通过在 static/src/js/tour.js 文件中添加此文件,来添加一个仅包含一个步骤的最简导航演示。

import { registry } from "@web/core/registry";


registry.category("web_tour.tours").add('estate_tour', {
    url: "/web",
    sequence: 170,
    steps: () => [{
    trigger: '.o_app[data-menu-xmlid="estate.menu_root"]',
    content: 'Start selling your properties from this app!',
    position: 'bottom',
    }],
});

您需要将该文件包含在清单文件(manifest file)中相应的包(bundle)里:

{
    "name": "Real Estate",
    # [...]
    "assets": {
        "web.assets_backend": [
            "estate/static/src/js/tour.js",
        ],
    },
}

注解

与普通 Python 模块不同,可导入模块不支持通配符扩展;因此,您需要明确列出要包含在模块中的每个文件。