编写可导入模块

Important

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

尽管作为开发者,我们更倾向于利用 Python 的全部能力来编写模块,但有时这是不可能的;典型情况就是诸如 Odoo.com 平台这类不允许部署自定义 Python 代码的托管方案。

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

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

问题描述

与 服务器框架 101 教程一样,我们将围绕房地产概念展开工作。

我们的目标是创建一个新的应用,以类似于 服务器框架 101 教程的方式(尽管更简单)来管理房地产。我们将在 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__.py 的 data 部分中列出文件,通常从模型文件开始。

__init__.py 文件是空的,但如果你想以经典方式(将其添加到 addons 路径中)部署模块,Odoo 需要靠它来识别模块。对于将被 导入 的模块,它并非严格必需,但保留它是一个好习惯。

部署模块

要部署模块,你需要创建该模块的 zip 文件并上传到你的 Odoo 实例。确保你的实例上已安装 base_import_module 模块,然后前往 Apps ‣ Import Module 并上传 zip 文件。你必须处于 开发者模式 才能看到 Import Module 菜单项。

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

上传模块时,向导会接受两个选项:

  • Force init:如果你的模块已安装并且你再次上传;勾选此选项将强制更新 XML 文件中所有标记为 noupdate="1" 的数据。

  • Import demo data:顾名思义

也可以使用 odoo-bin 命令行工具并配合 deploy 命令来部署模块:

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

该命令同样接受 --force 选项,其作用等同于向导中的 Force init 选项。

请注意,用于部署模块的用户必须拥有 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 :字段的类型(例如 char 、 integer 、 float 、 html 等)

  • required :字段是否必填(默认: False )

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

  • index :字段是否建立索引(默认: False )

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

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

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

Exercise

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

字段

类型

必填

x_date_availability

Date

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_id 、 name (界面中显示的名称)和 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 模型的定义。

Warning

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

安全

数据模块中的安全机制与 Python 模块完全相同,详见 第 4 章:安全简介 。

详情请参阅该教程。

Exercise

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

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

Tip

日志中的警告消息已经给出了大部分答案 ;-)

视图

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

由于视图和动作已经在 第 5 章:终于有 UI 可以玩了 和 第 6 章:基本视图 中定义过,这里不再详细展开。

Exercise

为 estate 模块添加一个基本 UI。

为 estate 模块添加一个基本 UI,允许用户查看、创建、编辑和删除房产。

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

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

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

  • 将视图添加到动作中。

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

关联关系

像 Odoo 这样的关系型系统的强大之处在于能够将记录相互链接。在普通的 Python 模块中,只需一行代码就可以在模型上定义新字段,将其链接到其他模型。在数据模块中,这仍然可行,但由于无法使用与 Python 中相同的语法,需要多做些工作。

如同 第 7 章:模型之间的关系 中那样,我们将为 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 ,包含以下字段:

    字段

    类型

    必填

    name

    Char

    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 (买方)

    Many2one (res.partner)

    x_user_id (销售人员)

    Many2one (res.users)

  5. 将新字段加入 x_estate.property 模型的表单视图中。

多对多

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

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

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

  • relation_table :用于该关系的数据表名称

  • column1 和 column2 :用于该关系的列名

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

Exercise

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

    字段

    类型

    必填

    name

    Char

    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 模型的表单视图中。

一对多

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

一对多字段的定义方式与多对一字段相同,只是将 ttype 设置为 one2many 。 relation 属性同样设置为要链接到的模型名称。还必须设置另一个属性来控制该关系:

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

Exercise

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

    字段

    类型

    必填

    值

    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. 创建一个列表视图和一个表单视图,包含 price、partner_id 和 status 字段。
    无需创建动作或菜单。
  4. 将字段 x_offer_ids 添加到您的 x_estate.property 模型及其表单视图中。

计算字段

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

参考 :与该主题相关的文档可参阅 计算字段 。

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

Note

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

对于计算方法,沙箱非常受限,仅提供允许代码执行所需的最基本实用工具。除了 Python 内置对象外,还可以使用 datetime 、 dateutil 和 time 模块(例如,用于协助日期计算)。

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

我们之前在 x_estate.property 模型上定义了两个“面积”字段: living_area 和 garden_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>

Note

在服务器动作中,您遍历的是 records 变量;而对于计算字段,您遍历的是 self 变量,其中包含该字段要计算其值的记录集。

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

与 Python 模块不同,计算字段默认会被存储。如果您希望某个计算字段不被存储(例如出于性能考虑或避免数据库膨胀),可以将 store 属性设置为 False。readonly 也一样:如果您希望计算字段不可编辑,需要将 readonly 属性设置为 True。

CDATA 部分用于告知 XML 解析器其中的内容是字符串而非 XML;这可以防止解析器将 Python 代码当作 XML 来解释,也避免代码在模块安装时写入数据库时被添加多余的空格等。

Exercise

  1. 为 x_estate.property 模型添加一个计算字段,返回 x_living_area 与 x_garden_area 字段之和,如上所示。

  2. 将该字段添加到 x_estate.property 模型的表单视图中。

Note

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

代码与业务逻辑

服务器动作

在 Python 模块中,您可以随意在模型上定义任何方法。一种常见的用法是为模型添加所谓的“动作”(action)方法,然后将这些方法绑定到界面中的按钮上(例如确认报价单、过账发票等)。

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

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

  • env:记录的环境

  • model:记录的模型

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

  • datetime、dateutil、timezone 和 time:用于辅助日期/时间计算的库

  • float_compare:用于按给定精度比较两个浮点值的工具函数

  • b64encode 和 b64decode:用于以 base64 编码和解码值的工具函数

  • Command:用于帮助构建复杂表达式和命令的工具类(参见 ORM 参考 中的 Command 类)

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

Note

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

如果安装了 website 模块,request 对象将在代码沙箱中可用,您可以用类似的方式将一个 response 对象赋值给 response 变量,从而向客户端返回响应。这将在 网站控制器 一节中详细介绍。

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

<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 rule)是一种基于特定触发器(如状态变更、添加标签等)自动对数据库中的记录执行操作的方式。它们可用于将行为与记录的生命周期事件关联起来,例如在报价被接受时发送一封电子邮件。

使用自动化规则来扩展标准行为可能比基于界面的方式更稳健,因为即使生命周期事件是通过按钮以外的方式触发的(例如通过 webhook 或直接调用方法,比如通过门户或电子商务确认报价单时),它也会运行。不过,要正确设置它们会稍微麻烦一些,因为需要通过设置要监视的特定字段等,来确保自动化只在恰当的时机运行。

文档:与该主题相关的更完整文档可参见 自动化任务。

Note

自动化规则不属于 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 实例本身中找到:在技术菜单中导航到相应的记录,然后使用调试菜单中的 View Metadata 菜单项。模型的 XML ID 就是模型名称将点替换为下划线并加上 model_ 前缀(例如 sale.model_sale_order 对应 sale 模块中定义的 sale.order);字段的 XML ID 则是模型名称将点替换为下划线,加上 field_ 前缀、模型名称和字段名(例如 sale.field_sale_order__name 是 sale.order 模型 name 字段的 XML ID,该字段定义于 sale 模块)。

网站控制器

Odoo 的 HTTP 控制器通常定义在模块的 controllers 目录中。在数据模块中,如果安装了 website 模块,可以定义行为类似控制器的服务器动作。

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

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

  • request.get_http_params():从查询字符串和请求体中的表单(application/x-www-form-urlencoded 和 multipart/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 工具进行 JSON 序列化;这对于通过 API 调用建立服务器间通信非常有用。

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

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

Note

服务器动作的 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 模块添加它们完全相同。

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

作为示例,我们为 Estate 模块添加一个简单的“漫游”(tour)。漫游是 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",
        ],
    },
}

Note

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