本文档介绍了 Odoo JavaScript 框架。该框架虽然在代码行数上不是一个大型应用程序,但它非常通用,因为它基本上是一个将声明性接口描述转换为实时应用程序的机器,能够与数据库中的每个模型和记录进行交互。甚至可以使用 Web 客户端修改 Web 客户端的界面。

JavaScript 框架旨在适用于三个主要使用场景:

  1. Web 客户端:这是私有的 Web 应用程序,用户可以在其中查看和编辑业务数据。它是一个单页应用程序(页面从不重新加载,仅在需要时从服务器获取新数据)。

  2. 网站:这是 Odoo 的公共部分。它允许未识别的用户浏览内容、购物或作为客户执行许多操作。这是一个经典的网站:包含各种路由、控制器以及一些使其工作的 JavaScript 代码。

  3. 销售点 (Point of Sale):这是销售点的接口。它是一个专门的单页应用程序。

有些 JavaScript 代码在这三个使用场景中是通用的,并被打包在一起(详见下文的资产部分)。本文档将主要关注 Web 客户端的架构。

Web 客户端是一个单页应用程序:用户每次执行操作时,客户端不需要从服务器请求整个页面,而只加载更新用户界面 (UI) 所需的部分内容。同时,它还负责更新 URL 中的信息,以便在大多数情况下,刷新页面或关闭浏览器再重新打开时,仍能显示相同的内容。

这里我们快速概述一下 Web 客户端代码,位于 web 插件中。路径将相对于 web/static/src 进行描述。以下描述并非详尽无遗;其目的仅是为读者提供架构的概览。

  • module_loader.js:这是定义 Odoo JavaScript 模块系统的文件,必须在加载任何其他 JS 模块之前加载。

  • core/:此文件夹包含 JavaScript 框架的最底层代码,可用于 Web 客户端、网站、门户和销售点应用程序。

  • webclient/:此文件夹包含特定于 Web 客户端的文件,无法在网站或销售点中使用,如操作管理器和操作服务。

  • webclient/webclient.js:这是 Web 客户端组件,主要是操作容器和导航栏的包装器,并处理应用启动时的一些必要操作,如加载 URL 状态。

  • webclient/actions/:此文件夹包含负责显示和切换操作的代码。

  • views/:此文件夹包含视图基础设施的代码,以及大多数视图(某些类型的视图由其他插件添加)。

  • views/fields/:包含各种字段组件的定义,以及多个字段使用的一些实用工具。

  • search/:这些文件定义了搜索视图(从 Web 客户端的角度来看,它不是一个视图,而是从服务器的角度来看是一个视图)。

文件无法正确加载可能有多种原因。以下是一些可能的解决方法:

  • 确保已保存文件;忘记保存文件是常见的错误。

  • 查看控制台(在开发工具中,通常通过 F12 打开)并检查是否有错误。

  • 尝试在文件开头添加 console.log(),以查看文件是否已加载。如果未加载,可能是不在正确的资产包中,或者资产包未更新。

根据您的设置,服务器在修改文件后可能不会重新生成资产包;有几种方法可以解决这个问题:

  • 重启服务器 会强制服务器在下一次请求时检查资产包是否已更新。

  • 调试模式下,调试菜单中有一个选项(导航栏中的按钮),可以强制服务器在不重启的情况下即时重新生成资产包。

  • 使用 --dev=xml 选项启动服务器 将强制服务器每次请求时检查资产包是否更新。建议在积极开发时使用此选项,但不建议在生产环境中使用。

  • 确保更改代码后刷新页面。Odoo 目前没有任何热模块重新加载机制。

大型应用程序通常被分解为多个较小的文件,这些文件需要相互连接。有些文件可能需要使用在其他文件中定义的代码。共享代码在文件之间有两种方式:

  1. 使用全局作用域(window 对象)读取/写入一些对象或函数的引用。
  2. 使用模块系统提供一种方式让每个模块导出或导入值,并确保它们按正确的顺序加载。

虽然可以在全局作用域中工作,但这样做存在一些问题:

  • 很难确保实现细节不被暴露:全局作用域中的函数声明对所有其他代码都可访问。
  • 只有一个命名空间,这样会有很大的命名冲突风险。
  • 依赖关系是隐式的:如果一段代码依赖于另一段代码,那么它们的加载顺序很重要,但很难保证。

使用模块系统有助于解决这些问题:因为模块指定了它们的依赖关系,模块系统可以按正确的顺序加载它们,或者在依赖关系缺失或循环依赖时发出错误。模块还形成自己的命名空间,可以选择导出什么,从而防止实现细节的暴露和命名冲突。

虽然我们可以直接使用 ECMAScript (ES) 模块,但这种方法有很多缺点:每个 ES 模块都需要一次网络往返,当你有数百个文件时,这会变得非常慢,并且 Odoo 中的许多文件需要存在,尽管没有被任何东西导入,因为它们只是添加了框架将使用的代码,而不是相反。

因此,Odoo 采用了一种资产包系统。在这些包中,JavaScript 文件是带有顶部特殊注释的 ES 模块。这些模块将被打包在一起,并转译以供我们的模块加载器使用。虽然你可以编写不使用此模块系统的代码,但通常不建议这样做。

以下是你提供内容的中文翻译:

markdown 复制代码

尽管我们尽力提供不需要修改类的扩展点,但有时仍有必要在原地修改现有类的行为。目标是拥有一种机制来更改一个类以及所有未来/当前的实例。这可以通过使用 patch 实用函数来实现:

/** @odoo-module */
import { Hamster } from "@web/core/hamster"
import { patch } from "@web/core/utils/patch";

patch(Hamster.prototype, {
    sleep() {
        super.sleep(...arguments);
        console.log("zzzz");
    },
});

在修补方法时,需要修补类的原型 (prototype),但如果你想修补类的静态属性,则需要修补类本身。

修补是一种危险的操作,应该谨慎进行,因为它会修改类的所有实例,即使这些实例已经被创建。为避免出现奇怪的问题,修补应尽早在模块的顶层进行。如果在运行时修补类,而该类已经被实例化,可能会导致极其难以调试的问题。

在 Odoo 生态系统中,一个常见的需求是从外部扩展或更改基础系统的行为(通过安装应用程序,即不同的模块)。例如,可能需要在某些视图中添加一个新的字段小部件。在这种情况下,以及许多其他情况下,通常的流程是创建所需的组件,然后将其添加到注册表中(注册步骤),以使 Web 客户端的其余部分知道它的存在。

系统中有几个可用的注册表。框架使用的注册表是主注册表中的类别,可以从 @web/core/registry 导入。

字段注册表包含所有 Web 客户端已知的字段小部件。每当视图(通常是表单或列表/看板)需要字段小部件时,就会在这里查找。一个典型的使用案例如下:

import { registry } from "@web/core/registry";
class PadField extends Component { ... }

registry.category("fields").add("pad", {
  component: PadField,
  supportedTypes: ["char"],
  // ...
});

此注册表包含所有 Web 客户端已知的 JavaScript 视图。

我们在此注册表中跟踪所有客户端操作。操作管理器在需要创建客户端操作时会在此查找。客户端操作可以是一个函数——在调用操作时会调用该函数,并且返回的值将作为后续操作执行(如果需要的话)——也可以是执行该操作时显示的 Owl 组件。

在 Web 客户端中,有些问题无法通过单个组件处理,因为这些问题是横向的,涉及多个组件,或者需要在应用程序的生命周期内维持某种状态。

服务 (Services) 是解决这些问题的一种方法:它们在应用程序启动时创建,通过 useService 钩子提供给组件使用,并且在应用程序的整个生命周期内保持活跃。

例如,我们有一个 orm 服务,其作用是允许与服务器上的业务对象进行交互。

以下是 orm 服务实现的简化示例:

import { registry } from "@web/core/registry";
export const OrmService = {
    start() {
        return {
            read(...) { ... },
            write(...) { ... },
            unlink(...) { ... },
            // ...
        }
    },
};
registry.category("services").add("orm", OrmService);

服务在环境中是可用的,但通常应该通过 useService 钩子来使用,这样可以防止在组件销毁后调用服务上的方法,并且如果组件在方法调用期间被销毁,还能防止进一步的代码执行。


class SomeComponent extends Component {
    setup() {
        this.orm = useService("orm");
    }
    // ...
    getActivityModelViewID(model) {
        return this.orm.call(model, "get_activity_view_id", this.params);
    }
}

在开发 Odoo 时,通常有两种使用场景:一种是需要调用(Python)模型上的方法(通过控制器 /web/dataset/call_kw),另一种是需要直接调用控制器(在某个路由上可用的控制器)。

调用 Python 模型上的方法是通过 orm 服务完成的:


 return this.orm.call("some.model", "some_method", [some, args]);

直接调用控制器是通过 rpc 服务完成的:


 return this.rpc("/some/route/", {
    some: param,
});

Odoo 框架有一种标准方式向用户传达各种信息:通知,这些通知会显示在用户界面右上角。通知的类型遵循 Bootstrap 的 Toasts:

  • info(信息):用于显示由于无法失败的操作而产生的某些信息反馈。
  • success(成功):用户执行了一个可能会失败但没有失败的操作。
  • warning(警告):用户执行的操作只能部分完成。如果出现了问题但不是用户直接导致的,或者不特别需要处理时,也可以使用此类型。
  • danger(危险):用户尝试执行某个操作,但未能完成。

通知还可以用于在不打扰用户工作流程的情况下询问问题。例如,通过 VOIP 接收到的电话:可以显示一个带有两个按钮(接受或拒绝)的粘性通知。

在 Odoo 中有两种方式显示通知:

  1. 通知服务 (Notification Service):允许组件通过调用 add 方法从 JavaScript 代码中显示通知。
  2. display_notification 客户端操作:允许通过 Python 触发通知的显示(例如,在用户点击 object 类型按钮时调用的方法中)。此客户端操作使用通知服务。

通知有一些选项:

  • title: string,可选。将作为标题显示在顶部。
  • message: string,可选。通知的内容。可以是 markup 对象,以显示格式化文本。
  • sticky: boolean,可选(默认值为 false)。如果为 true,通知将保留直到用户关闭它。否则,通知会在短时间后自动关闭。
  • type: string,可选(默认值为 warning)。决定通知的样式。可能的值包括:infosuccesswarningdanger
  • className: string,可选。这是一个 CSS 类名,将自动添加到通知中。虽然可以用于样式目的,但不建议使用。

以下是在 JavaScript 中显示通知的一些示例:

// 请注意,我们在文本上调用 _t 以确保其正确翻译。
this.notification.add({
    title: _t("Success"),
    message: _t("Your signature request has been sent.")
});
this.notification.add({
    title: _t("Error"),
    message: _t("Filter name is required."),
    type: "danger",
});

在 Python 中显示通知

# 请注意,我们在文本上调用 _(string) 以确保其正确翻译。
def show_notification(self):
    return {
        'type': 'ir.actions.client',
        'tag': 'display_notification',
        'params': {
            'title': _('Success'),
            'message': _('Your signature request has been sent.'),
            'sticky': False,
        }
    }

系统托盘是界面中导航栏的右侧部分,Web 客户端在此显示一些小部件,例如消息菜单。

当系统托盘由导航栏创建时,它会查找所有已注册的系统托盘项并将它们显示出来。

目前没有专门的 API 用于系统托盘项。它们是 Owl 组件,可以像其他组件一样与环境进行通信,例如通过与服务交互。

可以通过将它们添加到“systray”注册表来将项目添加到系统托盘:

import { registry } from "@web/core/registry"
class MySystrayComponent extends Component {
    ...
}
registry.category("systray").add("MySystrayComponent", MySystrayComponent, { sequence: 1 });

项目在系统托盘中的顺序是根据它们在系统托盘注册表中的顺序进行排列的。

一些翻译是在服务器端完成的(基本上所有由服务器渲染或处理的文本字符串),但静态文件中也有需要翻译的字符串。目前的工作方式如下:

  • 每个可翻译的字符串都使用特殊函数 _t 进行标记。
  • 这些字符串由服务器用于生成适当的 PO 文件。
  • 每当 Web 客户端加载时,它会调用路由 /web/webclient/translations,该路由返回所有可翻译术语的列表。
  • 在运行时,每当调用函数 _t 时,它会在这个列表中查找翻译,并返回翻译结果,如果找不到,则返回原始字符串。

请注意,翻译的详细解释可以参考文档《模块翻译》(Translating Modules)。

import { _t } from "@web/core/l10n/translation";

class SomeComponent extends Component {
    static exampleString = _t("this should be translated");
    ...
    someMethod() {
        const str = _t("some text");
    }
}

使用翻译函数时需要注意:作为参数传递的字符串不能是动态的,因为它是从代码中静态提取的,用于生成 PO 文件,并作为要翻译术语的标识符。如果需要在字符串中插入动态内容,_t 支持占位符:

import { _t } from "@web/core/l10n/translation";
const str = _t("Hello %s, you have %s unread messages.", user.name, unreadCount);

请注意,字符串本身是固定的。这允许翻译函数在使用插值之前检索翻译后的字符串。

Web 客户端需要从 Python 获取一些信息以正常工作。为了避免通过 JavaScript 进行额外的网络请求,这些信息直接被序列化到页面中,并可以通过 @web/session 模块在 JS 中访问。

当加载 /web 路由时,服务器会将这些信息注入到一个 <script> 标签中。这些信息是通过调用模型 ir.httpsession_info 方法获得的。你可以重写这个方法来向返回的字典中添加信息。

from odoo import models
from odoo.http import request

class IrHttp(models.AbstractModel):
    _inherit = 'ir.http'

    def session_info(self):
        result = super(IrHttp, self).session_info()
        result['some_key'] = get_some_value_from_db()
        return result

现在,可以通过在 JavaScript 中读取会话来获取该值:

import { session } from "@web/session";
const myValue = session.some_key;

请注意,这种机制旨在减少 Web 客户端准备所需的通信量。它仅适用于计算成本较低的数据(因为 session_info 调用较慢会延迟所有人的 Web 客户端加载),以及在初始化过程中需要尽早获取的数据。

“视图”这个词有不止一种含义。本节讨论的是视图的JavaScript代码设计,而不是架构结构或其他内容。

尽管视图只是Owl组件,但内置视图通常具有相同的结构:一个名为“SomethingController”的组件作为视图的根组件。该组件创建一个“模型”(负责管理数据的对象)的实例,并包含一个名为“renderer”的子组件来处理显示逻辑。

网页客户端体验的一个重要部分是编辑和创建数据。大部分工作是通过字段小部件完成的,这些小部件了解字段类型以及如何显示和编辑值的具体细节。

与列表视图类似,字段小部件也支持简单的装饰功能。装饰的目标是提供一种简单的方式,根据记录的当前状态来指定文本颜色。例如:

<field name="state" decoration-danger="amount &lt; 10000"/>

有效的装饰名称如下:

  • decoration-bf
  • decoration-it
  • decoration-danger
  • decoration-info
  • decoration-muted
  • decoration-primary
  • decoration-success
  • decoration-warning

每个 decoration-X 都会映射到一个 text-X 的CSS类,这是标准的Bootstrap CSS类(text-ittext-bf 例外,它们由Odoo处理,分别对应斜体和粗体)。请注意,decoration 属性的值应为有效的Python表达式,并将记录作为评估上下文进行评估。

我们在此记录所有默认可用的非关系字段,顺序不分先后。

这是整数类型字段的默认字段类型。

支持的字段类型:integer

选项:
  • type: 设置输入类型(默认是 “text”,可以设置为 “number”)

    在编辑模式下,该字段会渲染为一个带有HTML属性 type="number" 的输入框(这样用户可以利用本机支持功能,尤其是在移动设备上)。在这种情况下,默认格式化功能会被禁用,以避免不兼容问题。

  <field name="int_value" options="{'type': 'number'}" />
  • step: 设置步进值,当用户点击按钮时,数值将按步进值增减(仅适用于类型为 number 的输入框,默认值为1)。
<field name="int_value" options="{'type': 'number', 'step': 100}" />
  • format: 是否应对数字进行格式化(默认为 true)。

默认情况下,数字会根据区域设置参数进行格式化。此选项将阻止字段的值被格式化。

<field name="int_value" options='{"format": false}' />

这是浮点数类型字段的默认字段类型。

支持的字段类型:float

属性:

digits: 显示精度。

<field name="factor" digits="[42,5]" />
选项:
  • type: 设置输入类型(默认是 “text”,可以设置为 “number”)。

在编辑模式下,该字段会渲染为一个带有HTML属性 type=“number” 的输入框(这样用户可以利用本机支持功能,尤其是在移动设备上)。在这种情况下,默认格式化功能会被禁用,以避免不兼容问题。

<field name="int_value" options="{'type': 'number'}" />
  • step: 设置步进值,当用户点击按钮时,数值将按步进值增减(仅适用于类型为 number 的输入框,默认值为0.1)。
<field name="int_value" options="{'type': 'number', 'step': 0.1}" />
  • format: 是否应对数字进行格式化(默认为 true)。

默认情况下,数字会根据区域设置参数进行格式化。此选项将阻止字段的值被格式化。

<field name="int_value" options="{'format': false}" />

这个小部件的目的是正确显示表示时间间隔的浮点值(以小时为单位)。例如,0.5 应该格式化为 0:30,或者 4.75 对应 4:45。

支持的字段类型: float

这个小部件旨在正确显示通过选项中的因子转换后的浮点值。例如,如果数据库中保存的值是 0.5,因子是 3,则小部件的值应该格式化为 1.5。

支持的字段类型: float

这个小部件的目的是用一个包含可能值范围的按钮替换输入字段(这些值在选项中给出)。每次点击允许用户在范围内循环。其目的是将字段值限制为预定义的选择。此外,这个小部件支持像 float_factor 小部件一样的因子转换(范围值应为转换结果)。

支持的字段类型: float

<field name="days_to_close" widget="float_toggle" options="{'factor': 2, 'range': [0, 4, 8]}" />

这是布尔类型字段的默认字段类型。

支持的字段类型: boolean

这是字符类型字段的默认字段类型。

支持的字段类型: char

这是日期类型字段的默认字段类型。它由一个文本框和一个日期选择器组成。

支持的字段类型: date

选项
  • min_date / max_date: 设置接受值的日期限制。默认情况下,最早接受的日期是 1000-01-01,最晚的是 9999-12-31。接受的值是 SQL 格式的日期(yyyy-MM-dd HH:mm:ss)或 “today”。
<field name="datefield" options="{'min_date': 'today', 'max_date': '2023-12-31'}" />
  • warn_future: 如果值在未来(基于今天),则显示警告。
<field name="datefield" options="{'warn_future': true}" />

这是日期和时间类型字段的默认字段类型。值总是根据客户端的时区显示。

支持的字段类型: datetime

选项
  • 参考日期字段选项

  • rounding: 用于生成时间选择器中可用分钟的增量。这不会影响实际值,只会影响下拉选择框中可用选项的数量(默认值:5)。

    <field name="datetimefield" options="{'rounding': 10}" />
  • show_time: 当设置为 false 时,将隐藏日期时间字段中的时间部分。字段仍会接受日期时间值,但时间部分在用户界面中将被隐藏(默认值:true)。

    <field name="datetimefield" widget="datetime" options="{'show_time': false}" />

这个小部件允许用户从一个选择器中选择开始日期和结束日期。

支持的字段类型: date, datetime

选项
  • 参考日期字段或日期和时间字段选项

  • start_date_field: 用于获取/设置日期范围的开始值的字段(不能与 end_date_field 一起使用)。

    <field name="end_date" widget="daterange" options="{'start_date_field': 'start_date'}" />
  • end_date_field: 用于获取/设置日期范围的结束值的字段(不能与 start_date_field 一起使用)。

    <field name="start_date" widget="daterange" options="{'end_date_field': 'end_date'}" />
剩余天数字段 (remaining_days)

这个小部件可以用于日期和日期时间字段。在只读模式下,它显示字段值与今天之间的天数差值。在编辑模式下,小部件会变成一个普通的日期或日期时间字段。

支持的字段类型: date, datetime

这是金额类型字段的默认字段类型。用于显示货币。如果选项中指定了货币字段,它将使用该货币;否则,将回退到默认货币(在会话中)。

支持的字段类型: monetary, float

选项
  • currency_field: 另一个字段名称,应该是一个 many2one 字段,指向货币。

    <field name="value" widget="monetary" options="{'currency_field': 'currency_id'}" />

这是文本类型字段的默认字段类型。

支持的字段类型: text

这个字段的作用是作为一个句柄显示,并允许通过拖放来重新排序各种记录。

警告
  • 必须在排序记录的字段上指定句柄。

  • 在同一个列表中不支持多个句柄小部件的字段。

支持的字段类型: integer

这个字段显示电子邮件地址。使用它的主要原因是它在只读模式下渲染为具有适当 href 的锚标签。

支持的字段类型: char

这个字段显示电话号码。使用它的主要原因是它在只读模式下渲染为具有适当 href 的锚标签,但仅在某些情况下:我们只希望在设备可以拨打此特定号码时使其可点击。

支持的字段类型: char

这个字段显示 URL(在只读模式下)。使用它的主要原因是它渲染为具有适当 CSS 类和 href 的锚标签。

此外,锚标签的文本可以通过 text 属性自定义(不会改变 href 值)。

支持的字段类型: char

<field name="foo" widget="url" text="Some URL" />
选项
  • website_path: (默认值:false)默认情况下,小部件会强制(如果尚未如此)href 值以 “http://” 开头,除非此选项设置为 true,从而允许重定向到数据库的自有网站。