Alan Hou的个人博客

Odoo 17开发者指南第八章 高级服务器端开发技术

第 5 章 基础服务端开发 中,您学习了如何为模型类编写方法、如何从继承的模型扩展方法,以及如何使用记录集。本章将涉及更高级的主题,例如处理记录集的环境(environment)、在按钮点击时调用方法,以及使用 onchange 方法。本章的内容将帮助您管理更复杂的业务问题。您将学习如何通过结合视觉元素和阐明 Odoo 应用开发过程中创建交互式功能的过程来建立理解。

在本章中,我们将探讨以下内容:

技术要求

对于本章,您需要 Odoo 在线平台

本章中使用的所有代码都可以从本书的 GitHub 存储库下载,网址为 https://github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter08

更改执行操作的用户

在编写业务逻辑代码时,您可能需要使用不同的安全上下文执行某些操作。一个典型的案例是使用 superuser 权限执行操作,从而绕过安全检查。当业务需求要求对用户没有安全访问权限的记录进行操作时,就会出现这种需求。

本节将向您展示如何通过使用 sudo() 允许普通用户创建 room 记录。简而言之,我们将允许用户自己创建 room,即使他们没有权限创建和分配 room 记录。

准备工作

为了更容易理解,我们将添加一个新模型来管理宿舍房间(hostel room)。我们将添加一个名为 hostel.student 的新模型。您可以参考以下定义来添加此模型:

您需要添加一个表单视图、一个动作(action)和一个菜单项(menu item)才能从用户界面看到这个新模型。您还需要为宿舍添加安全规则,以便他们可以发行宿舍学生(hostel student)。如果您不知道如何添加这些内容,请参考 第 3 章 创建 Odoo 插件模块

或者,您可以从我们的 GitHub 代码示例中使用现成的初始模块以节省时间。该模块位于 Chapter08/00_initial_module 文件夹中。GitHub 代码示例可在 https://github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter08/00_initial_module 获取。

操作步骤…

如果您已经测试过该模块,您会发现只有拥有 hostel.room 访问权限的用户才能将房间标记为管理员(manager)。非宿舍用户不能自己创建房间;他们需要请求管理员用户

  1. 此用户拥有 Hostel Manager 访问权限,这意味着他们可以创建 Hostel Room 记录:


    图 8.1 – 此用户拥有 Hostel Manager 访问权限
    如下图所示,Hostel Manager 也可以创建 room 记录:


    图 8.2 – Hostel Manager 可以创建 room 记录
  2. 此用户拥有 Hostel User 访问权限:


    图 8.3 – 此用户拥有 Hostel User 访问权限
    他们只能看到 Hostel Room 记录:


    图 8.4 – Hostel User 只能看到 Hostel Room 记录
    假设我们想添加一个新功能,以便非宿舍用户可以自己为自己创建房间。我们将在不授予他们 hostel.room 模型访问权限的情况下完成此操作。
    因此,让我们学习如何让普通宿舍学生用户实现这一目标。
  3. action_assign_room() 方法添加到 hostel.room 模型:
  4. 在方法中,确保我们正在对单个记录进行操作:
  5. 如果学生未付费,则发出警告(确保您已在顶部导入 UserError):
  6. superuser 身份获取 hostel.room空记录集
  7. 使用适当的值创建新的 room 记录:
  8. 要从用户界面触发此方法,请将按钮添加到学生表单视图
  9. 重启服务器并更新 my_hostel 以应用给定的更改。更新后,您将在学生表单视图上看到一个 Assign Room 按钮,如下所示:


    图 8.5 – 学生表单视图上的 Assign Room 按钮

当您点击该按钮时,将创建一个新的 room 记录。这也适用于非宿舍用户。您可以通过以 demo 用户身份访问 Odoo 来测试这一点。

工作原理…

前三个步骤中,我们添加了一个名为 action_assign_room() 的新方法。当用户点击学生表单视图上的 Assign Room 按钮时,将调用此方法。

第 4 步中,我们使用了 sudo()。此方法返回一个新的记录集,其环境(environment)已修改,其中的用户具有 superuser 权限。当使用 sudo() 调用记录集时,环境将修改 environment 属性为 su,这表示环境的 superuser 状态。您可以通过 recordset.env.su 访问其状态。通过此 sudo 记录集进行的所有方法调用都使用 superuser 权限进行。为了更好地理解这一点,从方法中删除 .sudo(),然后点击 Assign Room 按钮。它将引发 Access Error,并且用户将不再具有该模型的访问权限。简单地使用 sudo()绕过所有安全规则

如果您需要特定用户,您可以传入一个包含该用户或该用户数据库 ID 的记录集,如下所示:

此代码片段允许您使用 public 用户搜索可见的房间。

更多内容…

使用 sudo(),您可以绕过访问权限和安全记录规则。有时,您可以访问多个原本应该隔离的记录,例如多公司环境中不同公司的记录。sudo() 记录集绕过 Odoo 的所有安全规则

如果您不小心,在此环境中搜索的记录可能会链接到数据库中存在的任何公司,这意味着您可能会向用户泄露信息;更糟糕的是,您可能会通过链接属于不同公司的记录而悄悄地破坏数据库

使用 sudo() 时,请务必谨慎,以避免意外后果,例如无意中链接来自不同公司的记录。在绕过访问权限之前,请确保适当的数据隔离,并考虑对数据完整性安全规则的潜在影响。

重要提示

使用 sudo() 时,始终仔细检查以确保您对 search() 的调用不依赖于标准记录规则来过滤结果。

不使用 sudo()search() 调用将遵守标准记录规则,可能会根据用户权限限制对记录的访问。这可能会导致搜索结果不完整或不准确,从而影响数据可见性和应用程序功能。

另请参阅

请查看以下参考资料以获取更多信息:

使用修改后的上下文调用方法

上下文(context)是记录集环境(environment of a recordset)的一部分。它用于从用户界面传递额外的信息,例如用户的时区语言。您还可以使用上下文来传递动作(actions)中指定的参数。标准 Odoo 附加模块中的几种方法使用上下文来根据这些上下文值调整其业务逻辑。有时有必要修改记录集值上的上下文,以从方法调用或**计算字段(computed field)**中获取所需的结果或所需的值。

本节将向您展示如何根据环境上下文中的值来更改方法的行为。

准备工作

本节中,我们将使用上一节中的 my_hostel 模块。在 hostel.room 模型的表单视图上,我们将添加一个按钮来移除房间成员。如果宿舍的普通住户在未经许可或授权的情况下将其他住户从他们分配的房间中移除,可能会在住宿内造成干扰和问题。请注意,我们已经在房间的表单视图中有了相同的按钮,但在这里,我们将探索 Odoo 中的上下文使用,深入了解它如何影响系统操作结果

操作步骤…

要添加按钮,您需要执行以下步骤:

  1. Remove Room Members 按钮添加到 hostel.room表单视图

  2. action_remove_room_members() 方法添加到 hostel.room 模型:

  3. 将以下代码添加到该方法中,以更改环境的上下文并调用方法以移除房间成员:

  4. 更新 hostel.student 模型的 action_remove_room() 方法,使其表现出不同的行为

工作原理…

在 Odoo 中,为了修改受上下文影响的行为,我们执行了以下操作:

第 1 步中,我们移除了房间成员

第 2 步中,我们添加了一个新按钮,Remove Room Members。用户将使用此按钮来移除成员

第 3 步第 4 步中,我们添加了一个方法,当用户点击 Remove Room Members 按钮时将调用该方法。

第 5 步中,我们使用了一些关键字参数调用了 student.with_context()。这返回了 room_id 记录集的一个新版本,其中包含更新的上下文。我们在这里向上下文中添加了一个键 is_hostel_room=True,但如果您愿意,可以添加多个键。我们在这里使用了 sudo()

第 6 步中,我们检查了上下文是否包含 is_hostel_room 键的正值。

现在,当宿舍房间在学生表单视图中移除房间成员时,room 记录集是 False

这只是一个修改上下文的简单示例,但您可以使用任何方法,例如 create()write()unlink() 等。您也可以根据您的要求使用任何自定义方法

更多内容…

也可以将字典传递给 with_context()。在这种情况下,该字典用作新的上下文,它会覆盖当前的上下文。因此,第 5 步也可以写成如下:

另请参阅

请参考以下小节以了解有关 Odoo 中上下文的更多信息:

执行原始 SQL 查询

大多数情况下,您可以使用 Odoo 的 ORM 来执行您想要的操作——例如,您可以使用 search() 方法来获取记录。但是,有时您需要更多;要么您无法使用域语法(domain syntax)表达您想要的内容(对于某些操作来说很棘手,即使不是完全不可能),要么您的查询需要多次调用 search(),这最终会效率低下

本李晨向您展示了如何使用原始 SQL 查询来获取用户在特定房间中持有的姓名金额

准备工作

本节中,我们将使用上一节中的 my_hostel 模块。为简单起见,我们将只在日志中打印结果,但在实际场景中,您需要在业务逻辑中使用查询结果。在 第 9 章《后端视图》 中,我们将在用户界面中显示此查询的结果。

操作步骤…

要获取有关用户在特定房间中持有的姓名和金额的信息,您需要执行以下步骤:

  1. action_category_with_amount() 方法添加到 hostel.room

  2. 在该方法中,编写以下 SQL 查询

  3. 执行查询:

  4. 获取结果并记录(确保您已导入 logger):

  5. hostel.room 模型的表单视图中添加一个按钮来触发我们的方法:

不要忘记在此文件中导入 logger。然后,重启更新 my_hostel 模块。

工作原理…

第 1 步中,我们添加了 action_category_with_amount() 方法,当用户点击 Log Category With Amount 按钮时,将调用此方法。

第 2 步中,我们声明了一个 SQL SELECT 查询。这将返回说明宿舍房间中金额的类别。如果您在 PostgreSQL CLI 中运行此查询,您将根据您的房间数据获得结果。这是基于我的数据库的示例数据:

第 4 步中,我们对存储在 self.env.cr 中的数据库游标调用了 execute() 方法。这会将查询发送到 PostgreSQL 并执行它。

第 5 步中,我们使用了游标的 fetchall() 方法来检索查询选择的行列表。此方法返回一个行列表。在我的例子中,这是 [('Single Room', 3000)]。从我们执行的查询形式来看,我们知道每行将恰好有两个值,第一个是 name,另一个是用户在特定房间中持有的金额。然后,我们简单地记录它。

第 6 步中,我们添加了一个 Add 按钮来处理用户操作

⚠️ 重要提示

如果您正在执行 UPDATE 查询,则需要手动使缓存失效,因为 Odoo ORM 的缓存不知道您使用 UPDATE 查询所做的更改。要使缓存失效,您可以使用 self.invalidate_cache()

更多内容…

self.env.cr 中的对象是 psycopg2 游标的薄包装器。以下是您大部分时间想要使用的方法:

警告

切勿自己进行替换;始终使用 %s 等格式化选项。如果您使用字符串连接等技术,可能会使代码容易受到 SQL 注入的攻击

处理原始 SQL 查询时要非常小心

另请参阅

有关访问权限管理,请参阅 第 10 章 安全访问

编写向导以引导用户

第 4 章 应用模型 中的 将抽象模型用于可重用模型功能 一节中,介绍了 models.TransientModel 基类。此类与普通模型有很多共同之处,只是瞬态模型(transient models)的记录会定期在数据库中清除,因此得名瞬态。这些用于创建向导(wizards)或对话框(dialogue boxes),用户在用户界面中填写这些内容,通常用于对数据库的持久化记录执行操作。

准备工作

本节中,我们将使用上一节中的 my_hostel 模块。我们将添加一个新的向导。通过这个向导,将为用户分配房间

操作步骤…

请按照以下步骤添加一个新的向导,用于更新分配房间和宿舍记录

  1. 向模块添加一个新的瞬态模型,其定义如下:

  2. 添加在瞬态模型上执行操作的回调方法(callback method)。将以下代码添加到 AssignRoomStudentWizard 类中:

  3. 为模型创建一个表单视图。将以下视图定义添加到模块视图

  4. 创建动作菜单条目以显示向导。将以下声明添加到模块菜单文件

  5. ir.model.access.csv 文件中为 assign.room.student.wizard 添加访问权限

  6. 更新 my_hostel 模块以应用更改。

工作原理…

第 1 步中,我们定义了一个新模型。除了基类是 TransientModel 而不是 Model 之外,它与其他模型没有什么不同。TransientModelModel 都共享一个共同的基类,称为 BaseModel,如果您检查 Odoo 的源代码,您会看到 99% 的工作都在 BaseModel 中,并且 ModelTransientModel几乎是空的

对于 TransientModel 记录,唯一改变的是:

在这种情况下使用 many2many 关系。如果 one2many 中的相关模型也是 TransientModel,您当然可以使用 one2many 字段。

我们在模型中定义了一个字段来存储房间。我们可以添加其他标量字段,以便我们可以记录预定的返回日期,例如。

第 2 步中,我们将代码添加到了向导类中,当点击第 3 步中定义的按钮时,将调用该代码。此代码读取向导中的值并更新 hostel.student 记录。

第 3 步中,我们为我们的向导定义了一个视图。有关详细信息,请参阅 第 9 章《后端视图》 中的 文档式表单(Document-style forms) 一节。这里的重点是页脚中的按钮type 属性设置为 'object',这意味着当用户点击按钮时,将调用名称由按钮的 name 属性指定的方法。

第 4 步中,我们确保在应用程序的菜单中有一个入口点用于我们的向导。我们在动作(action)中使用 target='new',以便表单视图作为对话框显示在当前表单之上。有关详细信息,请参阅 第 9 章 后端视图 中的 添加菜单项和窗口动作 一节:

图 8.6 – 为学生分配房间的向导

第 5 步中,我们为 assign.room.student.wizard 模型添加了访问权限。有了这个,管理员用户将获得 assign.room.student.wizard 模型的完全权限。

注意

Odoo v14 之前,TransientModel 不需要任何访问规则。任何人都可以创建记录,并且他们只能访问自己创建的记录。从 Odoo v14 开始,TransientModel 的访问权限是强制性的

更多内容…

以下是一些增强向导的技巧。

使用上下文计算默认值

我们介绍的向导要求用户在表单中填写成员姓名。我们可以使用 Web 客户端的一个功能来节省一些输入。执行动作时,**上下文(context)**会使用一些值进行更新,这些值可供向导使用:

这些值可用于计算模型默认值,甚至直接用于按钮调用的方法中。为了改进本节中的示例,如果我们在 hostel.room 模型的表单视图上显示一个按钮来启动向导,则向导创建的上下文将包含 {'active_model': 'hostel.room', 'active_id': <hostel_room_id>}。在这种情况下,您可以定义 room_id 字段,以获取由以下方法计算的默认值:

向导和代码复用

第 2 步中,我们可以在方法的开头添加 self.ensure_one(),如下所示:

我们建议在本节中使用 v17。它将允许我们通过为向导创建记录并将它们放在单个记录集中(请参阅 第 5 章 基础服务器端开发 中的 组合记录集 一节,了解如何执行此操作)来重用向导代码的其他部分,然后再在记录集上调用 add_room_in_student()。在这里,代码是微不足道的,您无需费尽周折地记录一些房间已被不同的学生分配。但是,在 Odoo 实例中,某些操作要复杂得多,并且拥有一个可以做正确事情的向导总是好的。使用这些向导时,请确保检查源代码中是否使用了上下文中的 active_model/active_id/active_ids 键。如果是这种情况,您需要传递一个自定义上下文(请参阅 使用修改后的上下文调用方法 一节)。

重定向用户

第 2 步中的方法不返回任何内容。这将导致在执行动作后向导对话框关闭。另一种可能性是让方法返回一个带有 ir.action 字段的字典。在这种情况下,Web 客户端将像用户单击了菜单条目一样处理动作。BaseModel 类中定义的 get_formview_action() 方法可用于实现此目的。例如,如果我们要显示宿舍房间的表单视图,我们可以编写如下内容:

这构建了一个包含来自此向导的房间的列表(实际上,当从用户界面调用向导时,只会有一个这样的房间)并创建了一个动态动作,该动作显示具有指定 ID 的房间。

重定向用户的技术可用于创建必须一步接一步执行多个步骤的向导。向导中的每一步都可以通过提供一个 Next 按钮来使用上一步的值。这将调用在向导上定义的方法,该方法会更新向导上的一些字段,返回一个将重新显示相同的更新向导的动作,并为下一步做好准备。

另请参阅

请参阅以下小节以获取更多详细信息:

定义 onchange 方法

在编写业务逻辑时,某些字段相互关联是很常见的情况。我们在 第 4 章《应用模型》 中的 为模型添加约束验证 一节中介绍了如何指定字段之间的约束。本节中说明了一个略有不同的概念。在这里,当在用户界面中修改字段时,会调用 onchange 方法,以更新 Web 客户端中记录的其他字段的值,通常在表单视图中。

我们将通过提供一个类似于 编写向导以引导用户 一节中定义的向导来说明这一点,但该向导可用于记录持续时间返回。当在表单视图中设置日期时,学生的持续时间会更新。虽然我们正在演示 Model 上的 onchange 方法,但这些功能也适用于普通的 Transient 模型。

准备工作

本节中,我们将使用本章 编写表单以引导用户 一节中的 my_hostel 模块。我们将创建一个宿舍学生并添加一个 onchange 方法,当用户选择出院日期(discharge date)或入院日期(admission date)字段时,该方法将自动填充持续时间。

您还需要通过定义以下模型来准备您的工作以进行表单视图

最后,您需要定义一个视图。这些步骤将留给您自己练习。

操作步骤…

自动填充当用户更改时返回的持续时间,您需要在 HostelStudent 步骤中添加一个 onchange 方法,其定义如下:

工作原理…

onchange 方法使用 @api.onchange 装饰器,该装饰器传入更改并将因此触发对方法调用的字段名称。在我们的示例中,如果用户修改了 admission_date(入学日期)或 discharge_date(出院日期),则会调用 onchange_duration 方法。

在方法主体内,我们计算了 持续时间 (duration),并使用属性赋值的方式更新了 表单视图 中的 duration 属性。

更多内容…

onchange 方法的局限性与记录状态

正如我们在本节中看到的,onchange 方法的基本用途是:当 用户界面 中某些字段发生更改时,计算并更新其他字段的新值。

在服务器端调用 onchange 方法

onchange 方法有一个限制:当您在 服务端 执行操作时,它不会被自动调用。onchange 仅在依赖操作通过 Odoo 用户界面 执行时才会自动触发。

然而,在某些情况下,必须调用这些 onchange 方法,因为它们会更新创建或更新记录中的重要字段。当然,您可以自己完成所需的计算,但这并非总是可行,因为 onchange 逻辑可能由您不了解的第三方插件模块添加或修改。

本节将讲解如何通过在创建记录之前手动调用 onchange 方法,来触发记录上的 onchange 逻辑。

准备工作

更改执行操作的用户 一节中,我们添加了一个 退房(Return Room)按钮,以便用户可以自行更新房间和宿舍。现在我们想对退回房间和宿舍执行同样的操作;我们将使用 分配房间(Assign Room)返回向导。

操作步骤…

在本节中,我们将手动更新 hostel.room 模型的一条记录。您需要执行以下步骤:

  1. hostel.student.py 文件中,从 tests 工具中导入 Form

  2. hostel.room 模型中创建 return_room 方法:

  3. 获取 assign.room.student.wizard空记录集

  4. 创建一个 wizardForm 块,如下所示:

  5. 通过分配房间来触发 onchange,然后返回更新后的 room_id 值:

工作原理…

onchange 方法主要从用户界面调用。但通过本节,我们学会了如何在服务器端使用/触发 onchange 方法的业务逻辑。这样,我们可以在创建记录时不绕过任何业务逻辑。

另请参阅

 

使用 compute 方法定义 onchange

前两节中,我们看到了如何定义和调用 onchange 方法,同时也看到了它的局限性:它只能从用户界面自动调用。作为该问题的解决方案,Odoo v13 引入了一种定义 onchange 行为的新方法。在本节中,我们将学习如何使用 compute 方法来产生类似于 onchange 方法的行为。

准备工作

在本节中,我们将使用上一节中的 my_hostel 模块。我们将用 compute 方法替换 hostel.student 上的 onchange 方法。

操作步骤…

请按照以下步骤使用 compute 方法修改 onchange 方法:

  1. onchange_duration() 方法中的 @api.onchange 替换为 @api.depends,如下所示:

  2. 在字段的定义中添加 compute 参数,如下所示:

  3. 升级 my_hostel 模块以应用代码,然后测试返回持续时间的表单以查看更改。

工作原理…

在功能上,我们计算的 onchange 工作方式与正常的 onchange 方法相似。唯一的区别是,现在 onchange 也将在后端更改时触发

另请参阅

基于 SQL 视图定义模型

在设计 插件模块 时,我们通过类对数据进行建模,然后 Odoo 的 ORM 将这些类映射到数据库表。我们遵循一些众所周知的设计原则,例如 关注点分离数据规范化

然而,在模块设计的后期阶段,聚合来自多个模型的数据到一个表,并在途中对其执行一些操作(尤其对于 报告仪表板 的生成)可能很有用。为了简化此过程并利用底层 PostgreSQL 数据库引擎的全部功能,可以在 Odoo 中定义一个 只读模型,该模型由一个 PostgreSQL 视图 而非表支持。

在本节中,我们将重用本章 编写向导来引导用户 一节中的房间模型,并创建一个新模型,以便更轻松地收集房间的 可用性作者 信息。

准备工作

在本节中,我们将使用上一节中的 my_hostel 模块。我们将创建一个名为 hostel.room.availability 的新模型来保存可用性数据。

操作步骤…

要创建一个由 PostgreSQL 视图支持的新模型,请执行以下步骤:

  1. 创建一个新模型,并将 _auto 类属性设置为 False

  2. 声明您希望在该模型中看到的字段,并将其设置为只读:

  3. 定义 init() 方法来创建视图:

  4. 您现在可以为新模型定义视图了。 数据透视表视图 (Pivot View) 对于探索数据特别有用(请参考 第 9 章 后端视图)。

  5. 不要忘记 为新模型定义一些访问规则(请查看 第 10 章 安全访问)。

工作原理…

重要提示: 忘记在视图定义查询中重命名列是一个常见的错误。这会导致 Odoo 找不到列时报错。另请注意,我们还需要提供一个名为 ID整数列值,其中包含唯一值

更多内容…

另请参阅

 

添加自定义设置选项

在 Odoo 中,您可以通过 “设置” 选项提供可选功能。用户可以随时启用或禁用此选项。在本节中,我们将演示如何创建 设置 选项。

准备工作

在前面的小节中,我们添加了按钮,以便宿舍用户可以点击并退回房间。但这并非适用于所有宿舍;因此,我们将创建一个 “设置” 选项来启用和禁用此功能。我们将通过隐藏这些按钮来实现。在本节中,我们将使用与前面小节相同的 my_hostel 模块。

操作步骤…

要创建自定义 设置 选项,请执行以下步骤:

  1. 通过继承 res.config.settings 模型来添加一个新字段:

  2. 使用 xpath 将此字段添加到现有 “设置”视图 中(有关详细信息,请参阅 第 9 章 后端视图):

  3. “设置” 添加操作和菜单:

  4. 重启服务器并更新 my_hostel 模块以应用更改。您将看到类似下图的配置选项。

图8.7 – 启用或禁用宾馆用户权限

工作原理…

更多内容…

管理 “设置” 选项还有其他几种方法:

设置 选项用于使您的应用程序更加通用。它们赋予用户自由度,允许他们随时启用或禁用功能。将功能转换为选项后,您可以用一个模块服务更多的客户,并且您的客户可以随时启用他们喜欢的功能。

实现初始化钩子 (Init Hooks)

第 6 章 管理模块数据 中,您学习了如何从 XML 或 CSV 文件添加、更新和删除记录。然而,有时业务案例很复杂,无法使用数据文件解决。在这种情况下,您可以使用 清单文件 中的 init 钩子 来执行您想要的操作。

复杂的业务案例可能需要超出标准 XML 或 CSV 文件的动态数据初始化。示例包括与外部系统集成、执行复杂计算或根据运行时条件配置记录,这些都可以通过清单文件中的 init 钩子 来实现。

准备工作

我们将使用与上一节相同的 my_hostel 模块。为简单起见,在本节中,我们将仅通过 post_init_hook 创建一些房间记录。

操作步骤…

要添加 post_init_hook,请执行以下步骤:

  1. 使用 post_init_hook 键在 __manifest__.py 文件中注册钩子:

  2. add_room_hook() 方法添加到 __init__.py 文件中:

工作原理…

在这个例子中,我们介绍了 post_init_hook,但 Odoo 支持另外两个钩子:

钩子 是在现有代码之前、之后或代替现有代码运行的函数。这些作为字符串显示的钩子函数包含在 Odoo 模块的 __init__.py 文件中。

退出移动版