编写可导入的模块¶
尽管作为开发者,我们更希望拥有 Python 的全部能力来编写模块,但有时无法做到;通常是在不允许部署自定义 Python 代码的托管解决方案上,例如 Odoo.com 平台。
不过,Odoo 的灵活性本就是为了让用户开箱即用地进行定制。虽然通过 Studio 可以完成许多定制,但也可以在 XML 数据文件 中定义模型、字段和逻辑。这样更便于开发、维护和部署这些定制。
在本教程中,我们将学习如何在 XML 数据文件中定义模型、字段和逻辑,并将它们打包成一个模块。这类模块有时被称为 可导入模块 或 数据模块。我们还会看到这种模块开发方式的局限性。
问题陈述¶
与 服务器框架入门 教程一样,我们将围绕房地产相关的概念展开。
我们的目标是创建一个新的应用来管理房地产物业,其方式与 服务器框架入门 教程类似(尽管更简单)。我们将使用 XML 数据文件而不是 Python 文件来定义模型、字段和逻辑。
在本教程结束时,我们的应用将能够实现以下功能:
管理待售的房地产物业
在网站上发布这些物业
通过网站在线接收报价
物业售出后向买方开具发票
模块结构¶
与任何开发项目一样,清晰的结构能让代码更易于管理和维护。
标准 Odoo 模块同时使用 Python 和 XML 文件,而数据模块只使用 XML 文件。因此,你的工作目录树大致会如下所示:
estate
├── actions
│ └── *.xml
├── models
│ └── *.xml
├── security
│ └── ir.access.csv
│ └── estate_security.xml
├── views
│ └── *.xml
├── __init__.py
└── __manifest__.py
你将只会有 __init__.py 和 __manifest__.py 这两个 Python 文件。__manifest__.py 文件与任何 Odoo 模块的清单文件相同,但还会在 data 列表中导入其模型。
请记住按依赖顺序在 __manifest__.py 的 data 部分列出文件,通常从模型文件开始。
__init__.py 文件是空的,但如果你希望以传统方式部署模块(将其添加到 addons 路径中),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 选项,它等同于向导中的 强制初始化 选项。
请注意,用于部署模块的用户必须拥有 Administration/Settings 访问权限。
练习
创建以下文件夹和文件:
/home/$USER/src/tutorials/estate/__init__.py/home/$USER/src/tutorials/estate/__manifest__.py
__manifest__.py文件应只定义模块的名称和依赖项。目前唯一必需的框架模块是base(以及base_import_module——严格来说你的模块并不 依赖 它,但你需要它才能导入你的模块)。创建模块的 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 模型定义。
练习
向表中添加以下基本字段:
字段 |
类型 |
必填 |
|---|---|---|
x_date_availability |
日期 |
|
x_expected_price |
浮动 |
True |
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_id、name (界面中显示的名称) 和 value (数据库中的值)。还可以设置 sequence 字段,用于控制这些选项在界面中的显示顺序(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_id 和 company_id 字段按公司或用户分别设置。这意味着,例如无法为 x_date_availability 字段设置“今天”这样的动态默认值。
安全¶
数据模块中的安全机制与 Python 模块完全相同,详见 第 4 章:安全性 - 简要介绍。
详情请参阅该教程。
练习
在适当的文件夹中创建
ir.access.csv文件,并在__manifest__.py文件中声明它。为
base.group_user用户组授予读取、写入、创建和删除权限。
小技巧
日志中的警告信息会告诉你大部分解决方法 ;-)
视图¶
视图是允许用户与数据交互的 UI 组件。它们在 XML 文件中定义,位于模块的 views 目录中。
由于视图和动作已在 第 5 章:终于有点界面可以玩了 和 第 6 章:基础视图 中介绍过,这里不再详述。
练习
为 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:应用于该关系的域过滤器
练习
创建一个新模型
x_estate.property.type,包含以下字段:字段
类型
必填
name字符型
True
为
x_estate.property.type模型添加动作、列表视图和菜单项。为
x_estate.property.type模型添加访问权限,以便用户访问。在
x_estate.property模型上创建以下字段:字段
类型
必填
x_property_type_idMany2one (
x_estate.property.type)True
x_partner_id(买方)Many2one (
res.partner)x_user_id(销售人员)Many2one (
res.users)在
x_estate.property模型的表单视图中包含这些新字段。
多对多¶
多对多是指向一组对象的关系。在我们的示例中,我们将定义指向新模型 x_estate.property.tag 的多对多关系。该标签表示物业的一个特征,例如:翻新、舒适等。
一个物业可以有多个标签,一个标签也可以分配给多个物业——这就是典型的多对多关系。
多对多字段的定义方式与多对一字段相同,但将 ttype 设为 many2many。relation 属性同样设置为要链接到的模型名称。还可以设置其他属性来控制这种关系:
relation_table:用于该关系的表名称column1和column2:用于该关系的列名称
这些属性是可选的,通常只在两个模型之间存在多个多对多字段时才需要指定,以避免冲突;在大多数情况下,Odoo ORM 能够自动确定正确的关联表和列。
练习
创建一个新模型
x_estate.property.tag,包含以下字段:字段
类型
必填
name字符型
True
为
x_estate.property.tag模型添加动作、列表视图和菜单项。为
x_estate.property.tag模型添加访问权限,以允许用户访问。在
x_estate.property模型上创建以下字段:字段
类型
x_property_tag_idsMany2many (
x_estate.property.tag)将该新字段添加到
x_estate.property模型的表单视图中。
一对多¶
一对多关系是指向对象列表的关系。在我们的示例中,我们将定义指向新模型 x_estate.property.offer 的一对多关系。该报价表示客户为购买房产而提出的报价。
一对多字段的定义方式与多对一字段相同,但需将 ttype 设为 one2many。relation 属性同样设置为要关联的模型名称。此外还必须设置另一个属性来控制该关系:
relation_field:关联模型上包含对父模型(多对一字段)引用的字段名称。它用于将两个模型关联起来。
练习
创建一个新模型
x_estate.property.offer,并包含以下字段:字段
类型
必填
值
x_price浮动
True
x_status选择
已接受、已拒绝
x_partner_idMany2one (
res.partner)True
x_property_idMany2one (
x_estate.property)True
为
x_estate.property.offer模型添加访问权限,以允许用户访问。- 创建一个包含 price、partner_id 和 status 字段的树视图和一个表单视图。无需创建动作或菜单。
将
x_offer_ids字段添加到你的x_estate.property模型及其表单视图中。
计算字段¶
计算字段是 Odoo 的核心概念,用于定义基于其他字段计算得出的字段。这对于派生自其他字段的字段很有用,例如子记录的汇总(将销售订单中所有明细的价格相加)。
参考:与本主题相关的文档请参阅 二进制字段。
数据模块可以定义任意类型的计算字段,但与 Python 模块相比相当受限。事实上,由于数据模块旨在部署在不允许运行任意代码的系统上,因此允许使用的 Python 代码非常有限。
注解
为数据模块编写的所有 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>
注解
在服务器动作中,你迭代的是 records 变量,而在计算字段的情况下,你迭代的是 self 变量,其中包含计算该字段所依据的记录集。
depends 属性用于定义计算字段所依赖的字段,compute 属性用于定义计算该字段所执行的代码(使用 Python 代码)。
与 Python 模块不同,计算字段默认是存储的。如果你希望计算字段不被存储(例如出于性能原因或避免数据库膨胀),可以将 store 属性设为 False。readonly 也是如此:如果你希望计算字段不可编辑,需要将 readonly 属性设为 True。
CDATA 部分用于向 XML 解析器说明该内容是字符串而非 XML;这可以防止解析器在模块安装时将代码插入数据库时试图把 Python 代码解释为 XML,或添加多余的空格等。
练习
按上面的示例,为
x_estate.property模型添加一个返回x_living_area与x_garden_area字段之和的计算字段。将该字段包含在
x_estate.property模型的表单视图中。
注解
与 Python 模块不同,无法为计算字段定义反向方法或搜索方法。
代码与业务逻辑¶
服务器动作¶
在 Python 模块中,你可以自由地在模型上定义任意方法。一种常见的使用模式是向模型添加所谓的“动作”方法,然后将这些方法绑定到界面中的按钮上(例如确认报价、过账发票等)。
在数据模块中,你可以通过定义绑定到模型的 服务器动作 来达到同样的效果。服务器动作代表在服务器上动态运行的逻辑片段。这些动作可以通过 菜单直接在数据库中进行手动配置,并且可以有不同的类型;在我们的示例中,我们将使用 code 类型,它允许我们在沙箱环境中运行任意 Python 代码。
此环境包含若干实用工具,可帮助你与 Odoo 数据库交互:
env:记录的运行环境model:记录所属的模型user与uid:当前用户及其 IDdatetime、dateutil、timezone和time:用于协助日期/时间计算的库float_compare:用于按给定精度比较两个浮点值的实用函数b64encode和b64decode:用于以 base64 编码和解码值的实用函数Command:用于帮助构建复杂表达式和命令的实用类(参见 ORM 参考 中的Command类)
此外,你可以通过 record 和 records 变量访问执行该动作所针对的记录集(通常从表单视图执行动作时为单条记录,从列表视图执行时为多条记录)。
注解
如果你的动作需要向客户端返回一个动作(例如将用户重定向到另一个视图),可以在服务器动作的代码中将其赋值给 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 模型的表单视图中,我们可以在表单视图的 header 中添加以下 按钮 节点:
<!-- 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 模型的齿轮图标()中,包括列表视图(勾选一条或多条记录时)和表单视图。
练习
为
x_estate.property.offer模型添加一个服务器动作,将报价的x_status字段设置为Accepted,并相应地更新该报价所属房产的销售价格和买家。该动作还应将同一房产上的所有其他报价标记为Refused。在报价的内嵌列表视图中添加一个按钮,用于执行此动作
覆盖 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>
通过自动化规则¶
自动化规则是一种基于特定触发器(如状态变化、添加标签等)自动对数据库中的记录执行动作的方式。它们可用于将行为与记录的生命周期事件关联起来,例如在报价被接受时发送电子邮件。
使用自动化规则来扩展标准行为可能比基于界面的方式更稳健,因为即使生命周期事件是通过按钮以外的方式触发的(例如通过 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 目录中。在数据模块中,如果安装了 website 模块,可以定义充当控制器的服务器动作。
安装 website 模块后,服务器动作可以标记为 在网站上可用 并指定一个路径(完整路径始终以 /website/action 为前缀,以避免 URL 冲突);全局 request 对象将在代码服务器动作的局部作用域中可用。
request 对象提供了若干方法来访问请求正文:
request.get_http_params():从查询字符串和正文中的表单(application/x-www-form-urlencoded和multipart/form-data)中提取键值对。request.get_json_data():从请求正文中提取 JSON 数据。
由于无法从服务器动作内部返回值,因此要定义返回的响应,可以将一个类似响应的对象赋值给 response 变量,它将被自动返回给网站。
下面是一个简单的网站控制器示例,当调用 URL /website/action/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)使用 QWeb 模板的 xmlid 渲染模板;额外的关键字参数会转发给werkzeug.Response对象(例如设置 cookie、header 等)request.redirect(location, code=303, local=True)重定向到其他 URL;local参数用于指定重定向是否相对于网站(默认:True)。request.not_found()返回一个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 模型。
练习
为你的模块添加一个 JSON API,使外部服务能够获取待售房产列表。
向模型添加一个新的
x_api_published字段,用于控制这些房产是否发布到 API 上添加一条访问权限记录,允许公共用户读取和写入该模型
通过为写操作添加一条域不可能成立的记录规则(例如
[('id', '=', False)])来阻止公共用户的任何写入添加一条记录规则,使标记为
x_api_published的房产可被公共用户读取添加一个服务器动作,在调用 URL
/website/action/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",
steps: () => [{
trigger: '.o_app[data-menu-xmlid="estate.menu_root"]',
content: 'Start selling your properties from this app!',
run: "click",
}],
});
然后你需要在清单文件中把该文件包含到合适的资源包中:
{
"name": "Real Estate",
# [...]
"assets": {
"web.assets_backend": [
"estate/static/src/js/tour.js",
],
},
}
你还需要在 data 文件夹中新建 estate_tour.xml 文件,添加一条 xml 记录,以便显示你的引导:
<record id="estate_tour" model="web_tour.tour">
<field name="name">estate_tour</field>
<field name="sequence">2</field>
<field name="rainbow_man_message">Welcome! Happy exploring.</field>
</record>
注解
与普通的 Python 模块不同,可导入模块不支持 glob 展开;因此你需要逐一列出要包含在模块中的每个文件。