在 第 5 章 基础服务端开发 中,您学习了如何为模型类编写方法、如何从继承的模型扩展方法,以及如何使用记录集。本章将涉及更高级的主题,例如处理记录集的环境(environment)、在按钮点击时调用方法,以及使用 onchange 方法。本章的内容将帮助您管理更复杂的业务问题。您将学习如何通过结合视觉元素和阐明 Odoo 应用开发过程中创建交互式功能的过程来建立理解。
在本章中,我们将探讨以下内容:
-
更改执行操作的用户
-
使用修改后的上下文调用方法
-
执行原始 SQL 查询
-
编写向导以引导用户
-
定义
onchange方法 -
在服务器端调用
onchange方法 -
使用
compute方法定义onchange -
定义基于 SQL 视图的模型
-
添加自定义设置选项
-
实现
init钩子
技术要求
对于本章,您需要 Odoo 在线平台。
本章中使用的所有代码都可以从本书的 GitHub 存储库下载,网址为 https://github.com/PacktPublishing/Odoo-17-Development-Cookbook-Fifth-Edition/tree/main/Chapter08。
更改执行操作的用户
在编写业务逻辑代码时,您可能需要使用不同的安全上下文执行某些操作。一个典型的案例是使用 superuser 权限执行操作,从而绕过安全检查。当业务需求要求对用户没有安全访问权限的记录进行操作时,就会出现这种需求。
本节将向您展示如何通过使用 sudo() 允许普通用户创建 room 记录。简而言之,我们将允许用户自己创建 room,即使他们没有权限创建和分配 room 记录。
准备工作
为了更容易理解,我们将添加一个新模型来管理宿舍房间(hostel room)。我们将添加一个名为 hostel.student 的新模型。您可以参考以下定义来添加此模型:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class HostelStudent(models.Model): _name = "hostel.student" _description = "Hostel Student Information" name = fields.Char("Student Name") gender = fields.Selection([("male", "Male"), ("female", "Female"), ("other", "Other")], string="Gender", help="Student gender") active = fields.Boolean("Active", default=True, help="Activate/Deactivate hostel record") hostel_id = fields.Many2one("hostel.hostel", "hostel", help="Name of hostel") room_id = fields.Many2one("hostel.room", "Room", help="Select hostel room") status = fields.Selection([("draft", "Draft"), ("reservation", "Reservation"), ("pending", "Pending"), ("paid", "Done"),("discharge", "Discharge"), ("cancel", "Cancel")], string="Status", copy=False, default="draft", help="State of the student hostel") admission_date = fields.Date("Admission Date", help="Date of admission in hostel", default=fields.Datetime.today) discharge_date = fields.Date("Discharge Date", help="Date on which student discharge") duration = fields.Integer("Duration", compute="_compute_check_duration", inverse="_inverse_duration", help="Enter duration of living") |
您需要添加一个表单视图、一个动作(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)。非宿舍用户不能自己创建房间;他们需要请求管理员用户:
- 此用户拥有 Hostel Manager 访问权限,这意味着他们可以创建 Hostel Room 记录:
图 8.1 – 此用户拥有 Hostel Manager 访问权限
如下图所示,Hostel Manager 也可以创建 room 记录:
图 8.2 – Hostel Manager 可以创建 room 记录 - 此用户拥有 Hostel User 访问权限:
图 8.3 – 此用户拥有 Hostel User 访问权限
他们只能看到 Hostel Room 记录:
图 8.4 – Hostel User 只能看到 Hostel Room 记录
假设我们想添加一个新功能,以便非宿舍用户可以自己为自己创建房间。我们将在不授予他们hostel.room模型访问权限的情况下完成此操作。
因此,让我们学习如何让普通宿舍学生用户实现这一目标。 - 将
action_assign_room()方法添加到hostel.room模型:
1234class HostelStudent(models.Model):_name = "hostel.student"...def action_assign_room(self): - 在方法中,确保我们正在对单个记录进行操作:
1self.ensure_one() - 如果学生未付费,则发出警告(确保您已在顶部导入
UserError):
12if self.status != "paid":raise UserError(_("You can't assign a room if it's not paid.")) - 以
superuser身份获取hostel.room的空记录集:
1room_as_superuser = self.env['hostel.room'].sudo() - 使用适当的值创建新的
room记录:
1234567room_rec = room_as_superuser.create({"name": "Room A-103","room_no": "A-103","floor_no": 1,"room_category_id": self.env.ref("my_hostel.single_room_categ").id,"hostel_id": self.hostel_id.id,}) - 要从用户界面触发此方法,请将按钮添加到学生表单视图:
12345<button name="action_assign_room"string="Assign Room"type="object"class="btn-primary"/> - 重启服务器并更新
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 的记录集,如下所示:
|
1 2 3 |
public_user = self.env.ref('base.public_user') hostel_room = self.env['hostel.room'].with_user(public_user) hostel_room.search([('name', 'ilike', 'Room 101')]) |
此代码片段允许您使用 public 用户搜索可见的房间。
更多内容…
使用 sudo(),您可以绕过访问权限和安全记录规则。有时,您可以访问多个原本应该隔离的记录,例如多公司环境中不同公司的记录。sudo() 记录集绕过 Odoo 的所有安全规则。
如果您不小心,在此环境中搜索的记录可能会链接到数据库中存在的任何公司,这意味着您可能会向用户泄露信息;更糟糕的是,您可能会通过链接属于不同公司的记录而悄悄地破坏数据库。
使用 sudo() 时,请务必谨慎,以避免意外后果,例如无意中链接来自不同公司的记录。在绕过访问权限之前,请确保适当的数据隔离,并考虑对数据完整性和安全规则的潜在影响。
重要提示
使用
sudo()时,始终仔细检查以确保您对search()的调用不依赖于标准记录规则来过滤结果。不使用
sudo(),search()调用将遵守标准记录规则,可能会根据用户权限限制对记录的访问。这可能会导致搜索结果不完整或不准确,从而影响数据可见性和应用程序功能。
另请参阅
请查看以下参考资料以获取更多信息:
-
如果您想了解有关环境的更多信息,请参阅 第 5 章 基础服务器端开发 中的 获取模型的空记录集 一节
-
有关访问控制列表和记录规则的更多信息,请查看 第 10 章 安全访问
使用修改后的上下文调用方法
上下文(context)是记录集环境(environment of a recordset)的一部分。它用于从用户界面传递额外的信息,例如用户的时区和语言。您还可以使用上下文来传递动作(actions)中指定的参数。标准 Odoo 附加模块中的几种方法使用上下文来根据这些上下文值调整其业务逻辑。有时有必要修改记录集值上的上下文,以从方法调用或**计算字段(computed field)**中获取所需的结果或所需的值。
本节将向您展示如何根据环境上下文中的值来更改方法的行为。
准备工作
本节中,我们将使用上一节中的 my_hostel 模块。在 hostel.room 模型的表单视图上,我们将添加一个按钮来移除房间成员。如果宿舍的普通住户在未经许可或授权的情况下将其他住户从他们分配的房间中移除,可能会在住宿内造成干扰和问题。请注意,我们已经在房间的表单视图中有了相同的按钮,但在这里,我们将探索 Odoo 中的上下文使用,深入了解它如何影响系统操作和结果。
操作步骤…
要添加按钮,您需要执行以下步骤:
-
将 Remove Room Members 按钮添加到
hostel.room的表单视图:12345<button name="action_remove_room_members"string="Remove Room Members"type="object"class="btn-primary"/> -
将
action_remove_room_members()方法添加到hostel.room模型:12def action_remove_room_members(self):... -
将以下代码添加到该方法中,以更改环境的上下文并调用方法以移除房间成员:
1student.with_context(is_hostel_room=True).action_remove_room() -
更新
hostel.student模型的action_remove_room()方法,使其表现出不同的行为:123def action_remove_room(self):if self.env.context.get("is_hostel_room"):self.room_id = False
工作原理…
在 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 步也可以写成如下:
|
1 2 3 |
new_context = self.env.context.copy() new_context.update({'is_hostel_room': True}) student.with_context(new_context) |
另请参阅
请参考以下小节以了解有关 Odoo 中上下文的更多信息:
-
第 5 章 基础服务器端开发 中的 获取模型的空记录集 一节解释了环境是什么
-
第 9 章《后端视图》 中的 向表单和动作传递参数 – context 一节解释了如何在动作定义中修改上下文
-
第 5 章 基础服务器端开发 中的 搜索记录 一节解释了活跃记录
执行原始 SQL 查询
大多数情况下,您可以使用 Odoo 的 ORM 来执行您想要的操作——例如,您可以使用 search() 方法来获取记录。但是,有时您需要更多;要么您无法使用域语法(domain syntax)表达您想要的内容(对于某些操作来说很棘手,即使不是完全不可能),要么您的查询需要多次调用 search(),这最终会效率低下。
本李晨向您展示了如何使用原始 SQL 查询来获取用户在特定房间中持有的姓名和金额。
准备工作
本节中,我们将使用上一节中的 my_hostel 模块。为简单起见,我们将只在日志中打印结果,但在实际场景中,您需要在业务逻辑中使用查询结果。在 第 9 章《后端视图》 中,我们将在用户界面中显示此查询的结果。
操作步骤…
要获取有关用户在特定房间中持有的姓名和金额的信息,您需要执行以下步骤:
-
将
action_category_with_amount()方法添加到hostel.room:12def action_category_with_amount(self):... -
在该方法中,编写以下 SQL 查询:
12345678910"""SELECThrc.name,hrc.amountFROMhostel_room AS hostel_roomJOINhostel_room_category as hrc ON hrc.id = hostel_room.room_category_idWHERE hostel_room.room_category_id = %(cate_id)s;""",{'cate_id': self.room_category_id.id} -
执行查询:
12345678910self.env.cr.execute("""SELECThrc.name,hrc.amountFROMhostel_room AS hostel_roomJOINhostel_room_category as hrc ON hrc.id = hostel_room.room_category_idWHERE hostel_room.room_category_id = %(cate_id)s;""",{'cate_id': self.room_category_id.id}) -
获取结果并记录(确保您已导入
logger):12result = self.env.cr.fetchall()_logger.warning("Hostel Room With Amount: %s", result) -
在
hostel.room模型的表单视图中添加一个按钮来触发我们的方法:1234<button name="action_category_with_amount"string="Log Category With Amount"type="object"class="btn-primary"/>
不要忘记在此文件中导入 logger。然后,重启并更新 my_hostel 模块。
工作原理…
在第 1 步中,我们添加了 action_category_with_amount() 方法,当用户点击 Log Category With Amount 按钮时,将调用此方法。
在第 2 步中,我们声明了一个 SQL SELECT 查询。这将返回说明宿舍房间中金额的类别。如果您在 PostgreSQL CLI 中运行此查询,您将根据您的房间数据获得结果。这是基于我的数据库的示例数据:
|
1 2 3 4 5 |
+---------------------------------------+-------+ | name | amount| |---------------------------------------+-------| | Single Room | 3000 | +---------------------------------------+-------+ |
在第 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 游标的薄包装器。以下是您大部分时间想要使用的方法:
-
execute(query, params):这会执行 SQL 查询,其中查询中标有%s的参数被params中的值替换,params是一个元组。
警告
切勿自己进行替换;始终使用
%s等格式化选项。如果您使用字符串连接等技术,可能会使代码容易受到 SQL 注入的攻击。
-
fetchone():这会从数据库中返回一行,包装在一个元组中(即使查询只选择了一列)。 -
fetchall():这会将数据库中的所有行作为元组列表返回。 -
dictfetchall():这会将数据库中的所有行作为字典列表返回,将列名映射到值。
处理原始 SQL 查询时要非常小心:
-
您正在绕过应用程序的所有安全性。确保您使用您正在检索的任何 ID 列表调用
search([('id', 'in', tuple(ids)]),以过滤掉用户无权访问的记录。 -
您正在进行的任何修改都绕过了附加模块设置的约束,除了在数据库级别强制执行的
NOT NULL、UNIQUE和FOREIGN KEY约束。对于任何计算字段重新计算触发器也是如此,因此您最终可能会破坏数据库。 -
避免
INSERT/UPDATE查询——通过查询插入或更新记录不会运行通过覆盖create()和write()方法编写的任何业务逻辑。它也不会更新存储的计算字段,并且 ORM 约束也将被绕过。
另请参阅
有关访问权限管理,请参阅 第 10 章 安全访问。
编写向导以引导用户
在 第 4 章 应用模型 中的 将抽象模型用于可重用模型功能 一节中,介绍了 models.TransientModel 基类。此类与普通模型有很多共同之处,只是瞬态模型(transient models)的记录会定期在数据库中清除,因此得名瞬态。这些用于创建向导(wizards)或对话框(dialogue boxes),用户在用户界面中填写这些内容,通常用于对数据库的持久化记录执行操作。
准备工作
本节中,我们将使用上一节中的 my_hostel 模块。我们将添加一个新的向导。通过这个向导,将为用户分配房间。
操作步骤…
请按照以下步骤添加一个新的向导,用于更新分配房间和宿舍记录:
-
向模块添加一个新的瞬态模型,其定义如下:
123class AssignRoomStudentWizard(models.TransientModel):_name = 'assign.room.student.wizard'room_id = fields.Many2one("hostel.room", "Room", required=True) -
添加在瞬态模型上执行操作的回调方法(callback method)。将以下代码添加到
AssignRoomStudentWizard类中:123456789def add_room_in_student(self):hostel_room_student = self.env['hostel.student'].browse(self.env.context.get('active_id'))if hostel_room_student:hostel_room_student.update({'hostel_id': self.room_id.hostel_id.id,'room_id': self.room_id.id,'admission_date': datetime.today(),}) -
为模型创建一个表单视图。将以下视图定义添加到模块视图:
1234567891011121314151617<record id='assign_room_student_wizard_form' model='ir.ui.view'><field name='name'>assign room student wizard form view</field><field name='model'>assign.room.student.wizard</field><field name='arch' type='xml'><form string="Assign Room"><sheet><group><field name='room_id'/></group></sheet><footer><button string='Update' name='add_room_in_student' class='btn-primary' type='object'/><button string='Cancel' class='btn-default' special='cancel'/></footer></form></field></record> -
创建动作和菜单条目以显示向导。将以下声明添加到模块菜单文件:
123456<record model="ir.actions.act_window" id="action_assign_room_student_wizard"><field name="name">Assign Room</field><field name="res_model">assign.room.student.wizard</field><field name="view_mode">form</field><field name="target">new</field></record> -
在
ir.model.access.csv文件中为assign.room.student.wizard添加访问权限:1access_assign_room_student_wizard_manager,access.assign.room.student.wizard.manager,model_assign_room_student_wizard,my_hostel.group_hostel_manager,1,1,1,1 -
更新
my_hostel模块以应用更改。
工作原理…
在第 1 步中,我们定义了一个新模型。除了基类是 TransientModel 而不是 Model 之外,它与其他模型没有什么不同。TransientModel 和 Model 都共享一个共同的基类,称为 BaseModel,如果您检查 Odoo 的源代码,您会看到 99% 的工作都在 BaseModel 中,并且 Model 和 TransientModel 都几乎是空的。
对于 TransientModel 记录,唯一改变的是:
-
记录会定期从数据库中移除,以便瞬态模型的表不会随时间增长。
-
您不允许在引用普通模型的
TransientModel实例上定义one2many字段,因为这将在持久模型上添加一个链接到瞬态数据的列。
在这种情况下使用 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)**会使用一些值进行更新,这些值可供向导使用:
-
active_model:这是与动作相关的模型名称。这通常是屏幕上显示的模型。 -
active_id:这表示单个记录处于活动状态,并提供该记录的 ID。 -
active_ids:如果选择了多条记录,这将是一个包含 ID 的列表。当触发动作时在树状视图中选择了多个项目时会发生这种情况。在表单视图中,您将获得[active_id]。 -
active_domain:这是向导将操作的附加域。
这些值可用于计算模型的默认值,甚至直接用于按钮调用的方法中。为了改进本节中的示例,如果我们在 hostel.room 模型的表单视图上显示一个按钮来启动向导,则向导创建的上下文将包含 {'active_model': 'hostel.room', 'active_id': <hostel_room_id>}。在这种情况下,您可以定义 room_id 字段,以获取由以下方法计算的默认值:
|
1 2 3 |
def _default_member(self): if self.context.get('active_model') == 'hostel.room': return self.context.get('active_id', False) |
向导和代码复用
在第 2 步中,我们可以在方法的开头添加 self.ensure_one(),如下所示:
|
1 2 3 4 5 6 7 8 9 |
def add_room_in_student(self): hostel_room_student = self.env['hostel.student'].browse( self.env.context.get('active_id')) if hostel_room_student: hostel_room_student.update({ 'hostel_id': self.room_id.hostel_id.id, 'room_id': self.room_id.id, 'admission_date': datetime.today(), }) |
我们建议在本节中使用 v17。它将允许我们通过为向导创建记录并将它们放在单个记录集中(请参阅 第 5 章 基础服务器端开发 中的 组合记录集 一节,了解如何执行此操作)来重用向导代码的其他部分,然后再在记录集上调用 add_room_in_student()。在这里,代码是微不足道的,您无需费尽周折地记录一些房间已被不同的学生分配。但是,在 Odoo 实例中,某些操作要复杂得多,并且拥有一个可以做正确事情的向导总是好的。使用这些向导时,请确保检查源代码中是否使用了上下文中的 active_model/active_id/active_ids 键。如果是这种情况,您需要传递一个自定义上下文(请参阅 使用修改后的上下文调用方法 一节)。
重定向用户
第 2 步中的方法不返回任何内容。这将导致在执行动作后向导对话框关闭。另一种可能性是让方法返回一个带有 ir.action 字段的字典。在这种情况下,Web 客户端将像用户单击了菜单条目一样处理动作。BaseModel 类中定义的 get_formview_action() 方法可用于实现此目的。例如,如果我们要显示宿舍房间的表单视图,我们可以编写如下内容:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def add_room_in_student(self): hostel_room_student = self.env['hostel.student'].browse( self.env.context.get('active_id')) if hostel_room_student: hostel_room_student.update({ 'hostel_id': self.room_id.hostel_id.id, 'room_id': self.room_id.id, 'admission_date': datetime.today(), }) rooms = self.mapped('room_id') action = rooms.get_formview_action() if len(rooms.ids) > 1: action['domain'] = [('id', 'in', tuple(rooms.ids))] action['view_mode'] = 'tree,form' return action |
这构建了一个包含来自此向导的房间的列表(实际上,当从用户界面调用向导时,只会有一个这样的房间)并创建了一个动态动作,该动作显示具有指定 ID 的房间。
重定向用户的技术可用于创建必须一步接一步执行多个步骤的向导。向导中的每一步都可以通过提供一个 Next 按钮来使用上一步的值。这将调用在向导上定义的方法,该方法会更新向导上的一些字段,返回一个将重新显示相同的更新向导的动作,并为下一步做好准备。
另请参阅
请参阅以下小节以获取更多详细信息:
-
有关为向导定义视图的更多详细信息,请参阅 第 9 章 后端视图 中的 文档式表单 一节
-
要了解有关视图和调用服务器端方法的更多信息,请参阅 第 9 章 后端视图 中的 添加菜单项和窗口动作 一节
-
有关为向导创建记录并将它们放在单个记录集中的更多详细信息,请参阅 第 5 章 基础服务器端开发 中的 组合记录集 一节
定义 onchange 方法
在编写业务逻辑时,某些字段相互关联是很常见的情况。我们在 第 4 章《应用模型》 中的 为模型添加约束验证 一节中介绍了如何指定字段之间的约束。本节中说明了一个略有不同的概念。在这里,当在用户界面中修改字段时,会调用 onchange 方法,以更新 Web 客户端中记录的其他字段的值,通常在表单视图中。
我们将通过提供一个类似于 编写向导以引导用户 一节中定义的向导来说明这一点,但该向导可用于记录持续时间返回。当在表单视图中设置日期时,学生的持续时间会更新。虽然我们正在演示 Model 上的 onchange 方法,但这些功能也适用于普通的 Transient 模型。
准备工作
本节中,我们将使用本章 编写表单以引导用户 一节中的 my_hostel 模块。我们将创建一个宿舍学生并添加一个 onchange 方法,当用户选择出院日期(discharge date)或入院日期(admission date)字段时,该方法将自动填充持续时间。
您还需要通过定义以下模型来准备您的工作以进行表单视图:
|
1 2 3 4 5 6 7 8 9 |
class HostelStudent(models.Model): _name = "hostel.student" _description = "Hostel Student Information" admission_date = fields.Date("Admission Date", help="Date of admission in hostel", default=fields.Datetime.today) discharge_date = fields.Date("Discharge Date", help="Date on which student discharge") duration = fields.Integer("Duration", inverse="_inverse_duration",help="Enter duration of living") |
最后,您需要定义一个视图。这些步骤将留给您自己练习。
操作步骤…
要自动填充当用户更改时返回的持续时间,您需要在 HostelStudent 步骤中添加一个 onchange 方法,其定义如下:
|
1 2 3 4 5 6 7 |
@api.onchange('admission_date', 'discharge_date') def onchange_duration(self): if self.discharge_date and self.admission_date: self.duration = (self.discharge_date.year - \ self.admission_date.year) * 12 + \ (self.discharge_date.month - \ self.admission_date.month) |
工作原理…
onchange 方法使用 @api.onchange 装饰器,该装饰器传入更改并将因此触发对方法调用的字段名称。在我们的示例中,如果用户修改了 admission_date(入学日期)或 discharge_date(出院日期),则会调用 onchange_duration 方法。
在方法主体内,我们计算了 持续时间 (duration),并使用属性赋值的方式更新了 表单视图 中的 duration 属性。
更多内容…
onchange 方法的局限性与记录状态
正如我们在本节中看到的,onchange 方法的基本用途是:当 用户界面 中某些字段发生更改时,计算并更新其他字段的新值。
-
访问范围: 在方法体内,您可以访问当前记录在视图中显示的字段,但不一定是模型的所有字段。
-
特殊状态: 这是因为
onchange方法可能在记录尚未存储到数据库中,正在用户界面中创建时就被调用。在onchange方法内部,self处于一种特殊状态:self.id不是一个整数,而是odoo.models.NewId的一个实例。 -
数据库限制: 因此,您不得在
onchange方法中进行任何数据库更改。因为用户最终可能会取消记录的创建,而onchange方法在编辑过程中所做的任何更改将不会被回滚。
在服务器端调用 onchange 方法
onchange 方法有一个限制:当您在 服务端 执行操作时,它不会被自动调用。onchange 仅在依赖操作通过 Odoo 用户界面 执行时才会自动触发。
然而,在某些情况下,必须调用这些 onchange 方法,因为它们会更新创建或更新记录中的重要字段。当然,您可以自己完成所需的计算,但这并非总是可行,因为 onchange 逻辑可能由您不了解的第三方插件模块添加或修改。
本节将讲解如何通过在创建记录之前手动调用 onchange 方法,来触发记录上的 onchange 逻辑。
准备工作
在 更改执行操作的用户 一节中,我们添加了一个 退房(Return Room)按钮,以便用户可以自行更新房间和宿舍。现在我们想对退回房间和宿舍执行同样的操作;我们将使用 分配房间(Assign Room)返回向导。
操作步骤…
在本节中,我们将手动更新 hostel.room 模型的一条记录。您需要执行以下步骤:
-
在
hostel.student.py文件中,从tests工具中导入Form:1from odoo.tests.common import Form -
在
hostel.room模型中创建return_room方法:12def return_room(self):self.ensure_one() -
获取
assign.room.student.wizard的空记录集:1wizard = self.env['assign.room.student.wizard'] -
创建一个
wizard的Form块,如下所示:1with Form(wizard) as return_form: -
通过分配房间来触发
onchange,然后返回更新后的room_id值:12return_form.room_id = self.env.ref('my_hostel.101_room')record = return_form.save()
工作原理…
-
步骤 1 到 3 的解释,请参考 第 5 章 基础服务器端开发 中的 创建新记录 一节。
-
在 步骤 4 中,我们创建了一个 虚拟表单 (
Form) 来处理onchange规范,类似于 图形用户界面 (GUI) 中的操作。 -
步骤 5 包含退回房间和宿舍的完整逻辑。
-
第一行,我们在向导中赋值了
room_id,这会触发向导模型上任何相关的onchange方法。 -
接着,我们调用表单的
save()方法,它返回一个向导记录,该记录包含了onchange逻辑计算出的所有更新值。 -
最后,我们调用
add_room_in_student()方法来执行逻辑,退回更新后的房间和宿舍。
-
onchange 方法主要从用户界面调用。但通过本节,我们学会了如何在服务器端使用/触发 onchange 方法的业务逻辑。这样,我们可以在创建记录时不绕过任何业务逻辑。
另请参阅
-
要了解更多关于创建和更新记录的信息,请参阅 第 5 章 基础服务器端开发 中的 创建新记录 和 更新记录集记录的值 一节。
使用 compute 方法定义 onchange
在前两节中,我们看到了如何定义和调用 onchange 方法,同时也看到了它的局限性:它只能从用户界面自动调用。作为该问题的解决方案,Odoo v13 引入了一种定义 onchange 行为的新方法。在本节中,我们将学习如何使用 compute 方法来产生类似于 onchange 方法的行为。
准备工作
在本节中,我们将使用上一节中的 my_hostel 模块。我们将用 compute 方法替换 hostel.student 上的 onchange 方法。
操作步骤…
请按照以下步骤使用 compute 方法修改 onchange 方法:
-
将
onchange_duration()方法中的@api.onchange替换为@api.depends,如下所示:123@api.depends('admission_date', 'discharge_date')def onchange_duration(self):... -
在字段的定义中添加
compute参数,如下所示:12duration = fields.Integer("Duration", compute="onchange_duration", inverse="_inverse_duration",help="Enter duration of living") -
升级
my_hostel模块以应用代码,然后测试返回持续时间的表单以查看更改。
工作原理…
在功能上,我们计算的 onchange 工作方式与正常的 onchange 方法相似。唯一的区别是,现在 onchange 也将在后端更改时触发。
-
步骤 1: 我们用
@api.depends替换了@api.onchange。当字段值更改时,这是重新计算方法所必需的。 -
步骤 2: 我们用
compute参数将方法注册到了字段上。现在,当admission_date或discharge_date更改时,Odoo ORM 将在服务器端或客户端(如果字段在视图中)调用onchange_duration来计算duration。 -
关于
readonly: 虽然计算字段默认是只读的,但如果您像上面示例一样同时定义了inverse方法(例如_inverse_duration),Odoo 会知道该字段是可写的,并将允许在表单视图中对其进行编辑和存储。
另请参阅
-
要了解更多关于计算字段的信息,请参阅 第 4 章 应用模型 中的 为模型添加计算字段 一节。
基于 SQL 视图定义模型
在设计 插件模块 时,我们通过类对数据进行建模,然后 Odoo 的 ORM 将这些类映射到数据库表。我们遵循一些众所周知的设计原则,例如 关注点分离 和 数据规范化。
然而,在模块设计的后期阶段,聚合来自多个模型的数据到一个表,并在途中对其执行一些操作(尤其对于 报告 或 仪表板 的生成)可能很有用。为了简化此过程并利用底层 PostgreSQL 数据库引擎的全部功能,可以在 Odoo 中定义一个 只读模型,该模型由一个 PostgreSQL 视图 而非表支持。
在本节中,我们将重用本章 编写向导来引导用户 一节中的房间模型,并创建一个新模型,以便更轻松地收集房间的 可用性 和 作者 信息。
准备工作
在本节中,我们将使用上一节中的 my_hostel 模块。我们将创建一个名为 hostel.room.availability 的新模型来保存可用性数据。
操作步骤…
要创建一个由 PostgreSQL 视图支持的新模型,请执行以下步骤:
-
创建一个新模型,并将
_auto类属性设置为False:123class HostelRoomAvailability(models.Model):_name = 'hostel.room.availability'_auto = False -
声明您希望在该模型中看到的字段,并将其设置为只读:
1234room_id = fields.Many2one('hostel.room', 'Room', readonly=True)student_per_room = fields.Integer(string="Student Per Rooom", readonly=True)availability = fields.Integer(string="Availability",readonly=True)amount = fields.Integer(string="Amount", readonly=True) -
定义
init()方法来创建视图:12345678910111213141516def init(self):tools.drop_view_if_exists(self.env.cr, self._table)query = """CREATE OR REPLACE VIEW hostel_room_availability AS (SELECTmin(h_room.id) as id,h_room.id as room_id,h_room.student_per_room as student_per_room,h_room.availability as availability,h_room.rent_amount as amountFROMhostel_room AS h_roomGROUP BY h_room.id);"""self.env.cr.execute(query) -
您现在可以为新模型定义视图了。 数据透视表视图 (Pivot View) 对于探索数据特别有用(请参考 第 9 章 后端视图)。
-
不要忘记 为新模型定义一些访问规则(请查看 第 10 章 安全访问)。
工作原理…
-
步骤 1: 通常,Odoo 会为定义的模型创建一个新表。这是因为在
BaseModel类中,_auto属性默认值为True。通过将此属性设置为False,我们告诉 Odoo 我们将自行管理表的创建。 -
步骤 2: 我们定义了将用于生成模型的字段。我们确保将它们标记为
readonly=True,以防止视图启用修改操作,因为 PostgreSQL 视图是只读的,您将无法保存这些修改。 -
步骤 3: 我们定义了
init()方法。该方法通常不执行任何操作;它在_auto_init()之后被调用(后者负责在_auto = True时创建表,否则不执行任何操作),我们使用它来创建一个新的 SQL 视图(或者在模块升级的情况下更新现有视图)。视图创建查询必须创建一个视图,其列名与模型的字段名匹配。
重要提示: 忘记在视图定义查询中重命名列是一个常见的错误。这会导致 Odoo 找不到列时报错。另请注意,我们还需要提供一个名为
ID的整数列值,其中包含唯一值。
更多内容…
-
在此类模型上也可以使用 计算字段 和 关联字段 (
related)。唯一的限制是这些字段不能被存储(因此,您不能使用它们来分组记录或搜索)。 -
如果您需要按基础用户分组,则需要通过将其添加到视图定义中来存储该字段,而不是使用关联字段。
另请参阅
-
要了解有关用户操作的 UI 视图,请参阅 第 9 章 后端视图。
-
要更好地了解访问控制和记录规则,请查看 第 10 章 安全访问。
添加自定义设置选项
在 Odoo 中,您可以通过 “设置” 选项提供可选功能。用户可以随时启用或禁用此选项。在本节中,我们将演示如何创建 设置 选项。
准备工作
在前面的小节中,我们添加了按钮,以便宿舍用户可以点击并退回房间。但这并非适用于所有宿舍;因此,我们将创建一个 “设置” 选项来启用和禁用此功能。我们将通过隐藏这些按钮来实现。在本节中,我们将使用与前面小节相同的 my_hostel 模块。
操作步骤…
要创建自定义 设置 选项,请执行以下步骤:
-
通过继承
res.config.settings模型来添加一个新字段:123class ResConfigSettings(models.TransientModel):_inherit = 'res.config.settings'group_hostel_user = fields.Boolean(string="Hostel User", implied_group='my_hostel.group_hostel_user') -
使用
xpath将此字段添加到现有 “设置”视图 中(有关详细信息,请参阅 第 9 章 后端视图):1234567891011121314151617181920212223242526<record id="res_config_settings_view_form" model="ir.ui.view"><field name="name">res.config.settings.view.form.inherit.hostel</field><field name="model">res.config.settings</field><field name="priority" eval="5"/><field name="inherit_id" ref="base.res_config_settings_view_form"/><field name="arch" type="xml"><xpath expr="//div[hasclass('settings')]" position="inside"><div class="app_settings_block" data-string="Hostel" string="Hostel" data-key="my_hostel" groups="my_hostel.group_hostel_manager"><h2>Hostel</h2><div class="row mt16 o_settings_container"><div class="col-12 col-lg-6 o_setting_box" id="hostel"><div class="o_setting_left_pane"><field name="group_hostel_user"/></div><div class="o_setting_right_pane"><label for="group_hostel_user"/><div class="text-muted">Allow users to hostel user</div></div></div></div></div></xpath></field></record> -
为 “设置” 添加操作和菜单:
12345678910<record id="hostel_config_settings_action" model="ir.actions.act_window"><field name="name">Settings</field><field name="type">ir.actions.act_window</field><field name="res_model">res.config.settings</field><field name="view_id" ref="res_config_settings_view_form"/><field name="view_mode">form</field><field name="target">inline</field><field name="context">{'module' : 'my_hostel'}</field></record><menuitem name="Settings" id="hostel_setting_menu" parent="hostel_main_menu" action="hostel_config_settings_action" sequence="50"/> -
重启服务器并更新
my_hostel模块以应用更改。您将看到类似下图的配置选项。
图8.7 – 启用或禁用宾馆用户权限
工作原理…
-
res.config.settings模型: 在 Odoo 中,所有设置选项都添加到res.config.settings模型中。这是一个瞬时模型 (TransientModel)。 -
步骤 1: 我们通过继承
res.config.settings模型添加了一个新的 布尔 字段。我们添加了implied_group属性,其值为my_hostel.group_hostel_user。当管理员启用或禁用此布尔字段选项时,该组将被分配或移除给所有 Odoo 用户。 -
步骤 2: “设置” 使用一个表单视图来显示选项,其外部 ID 为
base.res_config_settings_view_form。我们通过继承该视图并使用xpath将我们的选项添加进去。-
data-key="my_hostel"属性告诉 Odoo 这是我们模块的设置块。
-
-
步骤 3: 我们添加了操作和菜单,以便从用户界面访问配置选项。您需要从操作中传递
{'module': 'my_hostel'}上下文,以便在单击菜单时默认打开my_hostel模块的 “设置” 选项卡。 -
结果: 启用或禁用此选项时,Odoo 会在后台对所有 Odoo 用户应用或移除
implied_group。由于我们将my_hostel.group_hostel_user组添加到按钮上,按钮将根据用户是否拥有该组而显示或隐藏。我们将在 第 10 章 安全访问 中详细介绍安全组。
更多内容…
管理 “设置” 选项还有其他几种方法:
-
通过模块安装/卸载管理功能:
-
如果将新功能分离到新模块中(例如
my_hostel_extras),则可以添加一个以module_为前缀的布尔字段:12module_my_hostel_extras = fields.Boolean(string='Hostel Extra Features') -
启用/禁用此选项时,Odoo 会自动安装/卸载
my_hostel_extras模块。
-
-
使用系统参数管理设置:
-
此类数据存储在
ir.config_parameter模型中。您可以使用config_parameter属性来创建系统范围的全局参数:123digest_emails = fields.Boolean(string="Digest Emails",config_parameter='digest.default_digest_emails') -
数据将以
digest.default_digest_emails为键存储在 “设置” | “技术” | “参数” | “系统参数” 菜单下的 系统参数 中。
-
设置 选项用于使您的应用程序更加通用。它们赋予用户自由度,允许他们随时启用或禁用功能。将功能转换为选项后,您可以用一个模块服务更多的客户,并且您的客户可以随时启用他们喜欢的功能。
实现初始化钩子 (Init Hooks)
在 第 6 章 管理模块数据 中,您学习了如何从 XML 或 CSV 文件添加、更新和删除记录。然而,有时业务案例很复杂,无法使用数据文件解决。在这种情况下,您可以使用 清单文件 中的 init 钩子 来执行您想要的操作。
复杂的业务案例可能需要超出标准 XML 或 CSV 文件的动态数据初始化。示例包括与外部系统集成、执行复杂计算或根据运行时条件配置记录,这些都可以通过清单文件中的 init 钩子 来实现。
准备工作
我们将使用与上一节相同的 my_hostel 模块。为简单起见,在本节中,我们将仅通过 post_init_hook 创建一些房间记录。
操作步骤…
要添加 post_init_hook,请执行以下步骤:
-
使用
post_init_hook键在__manifest__.py文件中注册钩子:123...'post_init_hook': 'add_room_hook',... -
将
add_room_hook()方法添加到__init__.py文件中:123456from odoo import api, SUPERUSER_IDdef add_room_hook(cr, registry):env = api.Environment(cr, SUPERUSER_ID, {})room_data1 = {'name': 'Room 1', 'room_no': '01'}room_data2 = {'name': 'Room 2', 'room_no': '02'}env['hostel.room'].create([room_data1, room_data2])
工作原理…
-
步骤 1: 我们在清单文件中使用值
add_room_hook注册了post_init_hook。这意味着模块安装后,Odoo 将在__init__.py中查找add_room_hook方法。 -
步骤 2: 我们声明了
add_room_hook(cr, registry)方法,该方法将在模块安装后被调用。我们从此方法中创建了两条记录。在实际场景中,您可以在这里编写复杂的业务逻辑。
在这个例子中,我们介绍了 post_init_hook,但 Odoo 支持另外两个钩子:
-
pre_init_hook: 此钩子将在您开始安装模块时被调用。-
在
__manifest__.py中注册:123...'pre_init_hook': 'pre_init_hook_hostel',... -
在
__init__.py中添加方法:1234def pre_init_hook_hostel(env):env['ir.model.data'].search([('model', 'like', 'hostel.hostel'),]).unlink()
-
-
uninstall_hook: 此钩子将在您卸载模块时被调用。这主要用于您的模块需要垃圾回收机制的情况。-
在
__manifest__.py中注册:123...'uninstall_hook': 'uninstall_hook_user',... -
在
__init__.py中添加方法:123def uninstall_hook_user(env):hostel = env['res.users'].search([])hostel.write({'active': False})
-
钩子 是在现有代码之前、之后或代替现有代码运行的函数。这些作为字符串显示的钩子函数包含在 Odoo 模块的 __init__.py 文件中。