编写可导入模块

重要

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

尽管作为开发人员,我们更喜欢使用 Python 的全部功能来编写模块,但有时无法这样做;特别是在不允许部署自定义 Python 代码的托管解决方案上,例如 Odoo.com 平台。

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

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

问题陈述

服务器框架入门 教程类似,我们将围绕房地产概念展开工作。

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

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

  • 管理待售的房地产属性

  • 在网站上发布这些属性

  • 通过网站在线接受报价

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

模块结构

与任何开发项目一样,清晰的结构使代码更易于管理和维护。

与同时使用 Python 和 XML 文件的标准 Odoo 模块不同,数据模块仅使用 XML 文件。因此,您的工作树预计会如下所示:

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

您拥有的唯一 Python 文件是 __init__.py__manifest__.py 文件。 __manifest__.py 文件与其他 Odoo 模块相同,但也会在 data 列表中导入其模型。

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

__init__.py 文件为空,但如果要以经典方式(通过将其添加到插件路径中)部署模块,则需要此文件以便 Odoo 识别模块。对于将被 导入 的模块来说,这不是严格必要的,但保留它是一个良好的实践。

部署模块

要部署模块,您需要创建模块的 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 选项,它等同于向导中的 强制初始化 选项。

请注意,用于部署模块的用户必须具有 管理/设置 访问权限。

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 等)

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

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

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

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

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

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

Exercise

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

字段

类型

依赖

x_date_availability

日期

x_expected_price

Float

True

x_bedrooms

整数

x_living_area

整数

x_facades

整数

x_garage

Boolean

x_garden

Boolean

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 模块完全相同,可以在 第 4 章:安全性(Security)- 简介 中找到相关信息。

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

Exercise

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

  2. 为组 base.group_user 授予读取、写入、创建和删除权限。

小技巧

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

视图

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

由于视图和操作已经在 第 5 章:终于,可以玩转一些用户界面(UI)了第 6 章:基础视图(Basic Views) 中定义,我们在此不再赘述。

Exercise

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

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

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

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

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

  • 将视图添加到操作中。

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

关系

像 Odoo 这样的关系系统的核心能力在于能够将记录链接在一起。在普通的 Python 模块中,可以通过一行代码定义新字段来将模型与其他模型链接起来。在数据模块中,这仍然是可能的,但由于无法使用与 Python 相同的语法,需要更多的工作量。

正如在 第 7 章:模型之间的关系 中一样,我们将为 estate 模块添加一些关系。我们将添加以下链接:

  • 购买房产的客户

  • 出售房产的房地产代理

  • 房产类型:房屋、公寓、顶层公寓、城堡……

  • 描述房产的一组标签:舒适、翻新……

  • 收到的报价列表

多对一(Many-to-one)

多对一是指向另一个对象的简单链接。例如,为了定义一个指向 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,包含以下字段:

    字段

    类型

    依赖

    name

    字符

    True

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

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

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

    字段

    类型

    依赖

    x_property_type_id

    Many2one (x_estate.property.type)

    True

    x_partner_id (buyer)

    Many2one (res.partner)

    x_user_id (salesperson)

    Many2one (res.users)

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

多对多(Many-to-many)

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

一个房产可以有多个标签,一个标签也可以分配给多个房产——这是典型的多对多关系。

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

  • relation_table:用于关系的表名称

  • column1column2:用于关系的列名称

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

Exercise

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

    字段

    类型

    依赖

    name

    字符

    True

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

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

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

    字段

    类型

    x_property_tag_ids

    Many2many (x_estate.property.tag)

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

一对多(One-to-many)

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

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

  • relation_field:相关模型上包含对父模型引用(多对一字段)的字段名称。这用于将两个模型链接在一起。

Exercise

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

    字段

    类型

    依赖

    值(Values)

    x_price

    Float

    True

    x_status

    选择

    Accepted, Refused

    x_partner_id

    Many2one (res.partner)

    True

    x_property_id

    Many2one (x_estate.property)

    True

  2. 为模型 x_estate.property.offer 添加访问权限,授予用户访问权限。

  3. 使用价格、partner_id 和状态字段创建一个树形视图和表单视图。
    无需创建操作或菜单。
  4. 将字段 x_offer_ids 添加到您的 x_estate.property 模型及其表单视图中。

计算字段

计算字段是 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。同样适用于 readonly:如果您希望计算字段不可编辑,则需要将 readonly 属性设置为 True

CDATA 部分用于告诉 XML 解析器内容是字符串而不是 XML;这可以防止解析器尝试将 Python 代码解释为 XML,或者在模块安装时将代码插入数据库时添加额外空格等。

Exercise

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

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

注解

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

代码和业务逻辑

服务器操作

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

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

此环境包含一些实用工具,可帮助您与 Odoo 数据库交互:

  • env:记录的环境

  • model:记录的模型

  • useruid:当前用户及其 ID

  • datetimedateutiltimezonetime:用于日期/时间计算的库

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

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

  • Command:一个用于构建复杂表达式和命令的实用类(参见 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 字段设置为 已接受,并相应地更新所关联房产的销售价格和买家信息。此操作还应将同一房产上的其他所有报价标记为 已拒绝

  2. 在嵌入式报价列表视图中包含一个按钮,允许执行此操作。

../../_images/offer_accept_button.png

覆盖 Python 模型

通过 UI 元素

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

然而,在某些情况下,可以通过替换调用这些方法的 UI 元素,并在服务器操作中拦截对这些方法的调用来实现类似效果。

一个典型的例子是与 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>

通过自动化规则

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

使用自动化规则扩展标准行为比基于 UI 的方法更可靠,因为即使生命周期事件不是通过按钮触发(例如通过 Webhook 或直接调用方法;例如通过门户或电子商务确认报价时),它也会运行。然而,它们的设置稍微复杂一些,需要确保自动化只在适当的时刻运行,例如通过设置特定字段进行监控等。

文档:与此主题相关的更完整文档可以在 自动化规则 中找到。

注解

自动化规则不是 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__name 是在 sale 模块中定义的 sale.order 模型的 name 字段的 XML ID)。

网站控制器

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

当安装了网站模块时,服务器操作可以标记为 在网站上可用 并指定路径(完整路径始终以 /actions 为前缀以避免 URL 冲突);全局 request 对象将在代码服务器操作的本地作用域中可用。

request 对象提供了多种方法来访问请求主体:

  • request.get_http_params():从查询字符串和主体中的表单提取键值对(支持 application/x-www-form-urlencodedmultipart/form-data)。

  • request.get_json_data():从请求主体中提取 JSON 数据。

由于无法从服务器操作内部返回值,因此可以通过将类似响应的对象赋值给 response 变量来定义要返回的响应,该变量将自动返回到网站。

以下是一个简单的网站控制器示例,当调用 URL /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 对象(例如,用于设置 Cookie、标头等)。

  • 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 工具进行序列化;这可用于通过 API 调用设置服务器到服务器的通信。

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

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

注解

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

Exercise

为您的模块添加一个 JSON API,以便外部服务可以检索待售房产的列表。

  1. 向模型添加一个新的 x_api_published 字段,用于控制房产是否发布到 API 中。

  2. 添加一个访问权限记录,允许公共用户读取和写入模型。

  3. 通过为写入操作添加一条带有不可能域(例如 [('id', '=', False)])的记录规则,防止公共用户的任何写入操作。

  4. 添加一条记录规则,使标记为 x_api_published 的房产可以被公共用户读取。

  5. 添加一个服务器操作,在调用 URL /actions/api/estate 时以 JSON 格式返回房产列表。

一点 JavaScript

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

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

例如,让我们为房地产模块添加一个简单的“引导”。引导是 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',
    }],
});

然后,您需要在清单文件中将该文件包含到适当的捆绑包中:

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

注解

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