编写可导入模块

重要

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

尽管作为开发者,我们更倾向于利用 Python 的全部功能来编写模块,但有时这是不可能的;特别是在托管解决方案中,这些方案不允许部署自定义 Python 代码,例如 Odoo.com 平台。

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

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

问题陈述

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

我们的目标是创建一个新的应用程序,以类似于(尽管更简单):doc:server_framework_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 部分中,按依赖顺序列出文件,通常从模型文件开始。

The __init__.py file is empty, but is required for Odoo to recognize the module if you ever want to deploy your module in the classic way (by adding it in an addons path). It is not strictly necessary for modules that will be imported, but it is a good practice to keep it.

部署模块

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

如果修改了模块,您需要创建一个新的 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

浮点数

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 字段设置动态默认值 “today”。

安全性

数据模块中的安全性与 Python 模块完全相同,可以在 第4章:安全 - 简要介绍 中找到。

请参阅该教程以获取详细信息。

Exercise

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

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

小技巧

日志中的警告信息已经给出了大部分解决方案 ;-)

查看

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

由于视图和操作已在 第5章:终于,有了可以操作的UI第6章:基础视图 中定义,此处不再赘述。

Exercise

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

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

  • 为模型 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

    字符

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

  3. x_estate.property.type 模型添加访问权限,以便用户能够访问。

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

    字段

    类型

    必填

    x_property_type_id

    Many2one (x_estate.property.type)

    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:用于关系的表的名称

  • column1column2: 用于关系的列的名称

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

Exercise

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

    字段

    类型

    必填

    name

    字符

  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 设置为 one2manyrelation 属性也设置为要链接的模型名称。必须设置另一个属性来控制关系:

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

Exercise

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

    字段

    类型

    必填

    x_price

    浮点数

    x_status

    选择

    已接受,已拒绝

    x_partner_id

    Many2one (res.partner)

    x_property_id

    Many2one (x_estate.property)

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

  3. 创建一个包含 price 、 partner_id 和 status 字段的树视图和表单视图。
    无需创建操作或菜单。
  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

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

Exercise

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

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

注解

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

代码与业务逻辑

服务器动作

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

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

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

  • self: 执行操作的记录

  • env: 记录的环境

  • model: 记录的模型

  • useruid:当前用户及其 ID

  • datetime, dateutil, timezonetime:用于帮助进行日期/时间计算的库

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

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

  • Command:一个实用工具类,用于帮助构建复杂的表达式和命令(请参阅 ORM 参考 中的 Command 类)

此外,你可以通过 recordrecords 变量访问执行操作的记录集(通常在从表单视图执行操作时是单个记录,而在从列表视图执行操作时是多个记录)。

注解

如果你的操作需要向客户端返回一个操作(例如将用户重定向到另一个视图),你可以在服务器操作代码中将其赋值给一个 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 模型的表单视图中将此操作包含为一个按钮,我们可以在表单视图的头部添加以下 button 节点:

<!-- 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 模型

通过 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 IDs 可以通过在 Odoo 实例中导航到技术菜单中的相应记录并使用调试菜单中的 View Metadata 菜单项来找到。模型的 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 为前缀,以避免URL冲突);全局的 request 对象在代码服务器动作的局部作用域中可用。

The 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 对象(例如用于设置 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 序列化;这对于通过 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 模块中完全相同。

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

举个例子,让我们在 Estate 模块中添加一个简单的 ‘tour’。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",
        ],
    },
}

注解

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