高级服务器端开发技术
在这一章中,我们将看到以下内容:
- 更改执行操作的用户
- 使用修改过的上下文调用方法
- 执行原始SQL查询
- 编写向导来指导用户
- 定义onchange方法
- 在服务器端调用onchange方法
- 定义基于SQL视图的模型
- 添加自定义设置选项
- 执行init钩子
在编写业务逻辑代码时,可能必须使用不同的安全上下文执行一些操作。典型的情况是使用管理员的权限执行操作,管理员可以绕过安全检查。此内容将向您展示如何让普通用户使用sudo()修改公司的电话号码。
准备
为了更容易理解,我们将添加一个新的模型来管理图书评级。我们将添加一个名为library.book.rent的新模型。您可以参考以下定义来添加此模型:
class LibraryBookRent(models.Model):
_name = \'library.book.rent\'
book_id = fields.Many2one(\'library.book\', \'Book\', required=True)
borrower_id = fields.Many2one(\'res.partner\', \'Borrower\',required=True)
state = fields.Selection([(\'ongoing\', \'Ongoing\'), (\'returned\',\'Returned\')],
\'State\', default=\'ongoing\',required=True)
book_id = fields.Many2one(\'library.book\', \'Book\', required=True)
rent_date = fields.Date(default=fields.Date.today)
return_date = fields.Date()
您需要添加一个表单视图、一个操作和一个菜单项,以便从用户界面查看这个新模型。您还需要为图书管理员添加安全规则,以便他们可以发行图书出租。请参考第4章,创建Odoo模块,如果你不知道如何添加这些东西。
或者,您可以使用GitHub代码示例中现成的初始模块来节省时间。这个模块将在Chapter09/r0_initial_module文件夹中可用。GitHub代码示例可在以下网站获得https://github.com/PacktPublishing/Odoo-12-Development-Cookbook-Third-Edition。
怎么做呢?
如果您测试了模块,您将发现只有拥有图书管理员访问权限的用户才能将一本书标记为已借。非图书馆员不得自行借阅图书;他们需要询问图书管理员的用户。假设我们想要添加一个新特性,以便非图书管理员的用户可以自己借书。我们将在不给它们library.book.rent模型的访问权限的情况下执行此操作。
要让普通用户借阅图书,需要执行以下步骤:
1. 在library.book模型中添加book_rent()方法:
class LibraryBook(models.Model):
_name = \'library.book\'
...
def book_rent(self):
2. 在该方法中,确保我们对单个记录进行操作:
self.ensure_one()
3.如果某本书无法借阅,请提出警告:
if self.state != \'available\':
raise UserError(_(\'Book is not available for renting\'))
4. 获取library.book.rent作为超级用户的空记录集:
rent_as_superuser = self.env[\'library.book.rent\'].sudo()
5. 使用适当的值创建一个新的图书借阅记录:
rent_as_superuser.create({
\'book_id\': self.id,
\'borrower_id\': self.env.user.partner_id.id,
})
6. 要从用户界面触发此方法,请在图书的表单视图中添加按钮:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary"/>
重新启动服务器并更新my_library以应用给定的更改。更新之后,您将在book表单视图上看到一个Rent this book按钮。当您单击它时,将创建一个新的租金记录。这也适用于非图书管理员用户。您可以通过访问Odoo作为演示用户来测试这一点。
它是如何工作的…
在步骤4中,我们使用了sudo()。这个方法返回一个新的记录集,其中的用户与self中的用户不相同。当不带参数调用时,sudo()将把Odoo超级用户(管理员)链接到环境中。通过这个sudo记录集进行的所有方法调用都是在新环境中进行的,因此都具有超级用户特权。为了更好地理解这一点,可以从方法中删除.sudo(),然后单击Rent this book按钮。它将引发访问错误,用户将不再拥有对模型的访问权。简单地说,sudo()将绕过所有安全规则。
如果您需要一个特定的用户,您可以传递一个包含该用户或该用户的数据库id的记录集。下面的代码片段允许你搜索可见的图书,使用public用户:
public_user = self.env.ref(\'base.public_user\')
public_book = self.env[\'library.book\'].sudo(public_user)
当您使用sudo()时,没有动作的可跟踪性,例如谁创建或更新了一条记录。在我们的内容中,公司最后一次修改的作者将是管理员,而不是最初调用create的用户。
可以使用在https://github.com/OCA/server-backend/找到的社区附加组件base_suspend_security来解决这个限制。
有更多的…
当使用不带参数的sudo()时,您将上下文的用户设置为Odoo超级用户。这个超级用户绕过了Odoo的所有安全规则,包括访问控制列表和记录规则。默认情况下,该用户还将company_id字段设置为实例的主公司(ID为1的公司)。
如果不小心,在此环境中创建的新记录将链接到超级用户的公司。
如果您不小心,在此环境中搜索的记录可能会链接到数据库中出现的任何公司,这意味着您可能会将信息泄露给真正的用户;更糟糕的是,你可能在默默地腐蚀通过将属于不同公司的记录链接到数据库中。
在使用sudo()时,始终要仔细检查以确保对search()的调用不依赖于标准记录规则来过滤结果,并确保对create()的调用不依赖于使用某些当前用户字段计算的默认值,比如company_id。
使用sudo()还涉及创建一个新的环境实例。这个环境将有一个初始为空的记录集缓存,并且该缓存将独立于self.env的缓存演化。这可能会导致虚假的数据库查询。在任何情况下,您都应该避免在循环内部创建新环境,并尝试将这些创建的环境移动到可能的最外层范围。
另请参阅
查看这些参考资料以获得更多信息:如果您想了解更多关于环境的信息,请参阅第6章基本服务器端开发中的获取模型配制的空记录集。有关访问控制列表和记录规则的更多信息,请参阅第11章访问安全。
context是记录集环境的一部分。它用于从用户界面传递诸如用户的时区和语言等信息,以及在操作中指定的上下文参数。标准外接程序中的许多方法使用上下文来调整它们的行为以适应这些值。有时需要修改记录集上的上下文,以便从方法调用中获得所需的结果或计算字段的所需值。此内容将展示如何在上下文的帮助下从同一方法获得不同的输出。
准备
对于这个内容,我们将使用上一个内容中的my_library模块。在library.book.rent模型的表单视图中,我们将添加一个按钮,用于将图书标记为丢失,以防普通用户丢失图书。注意,我们在书的表单视图中已经有了相同的按钮,但是在这里,我们将有一个稍微不同的行为来理解Odoo中上下文的使用。
怎么做呢?
要添加按钮,需要执行以下步骤:
1. 更新state字段的定义,使其具有lose状态:
state = fields.Selection([(\'ongoing\', \'Ongoing\'),
(\'returned\', \'Returned\'),
(\'lost\',\'Lost\')],
\'State\',
default=\'ongoing\',
required=True)
2. 在library.book.rent的表单视图中添加标记为丢失按钮:
<button name="book_lost" string="Lost the Book" states="ongoing" type="object"/>
3.在library.book.rent模型中添加book_lost()方法:
def book_lost(self):
...
4. 在这个方法中,确保我们操作的是单个记录,然后更改state:
self.ensure_one()
self.state = \'lost\'
5. 在方法中添加以下代码来更改环境的conext,并调用该方法将书的state更改为lost:
book_with_different_context = self.book_id.with_context(avoid_deactivate=True)
book_with_different_context.make_lost()
6. 更新library.book模型的make_lost()方法,使其具有不同的行为:
def make_lost(self): self.ensure_one() self.state = \'lost\' if not self.env.context.get(\'avoid_deactivate\'): self.active = False
它是如何工作的…
第5步使用一些关键字参数调用self.book_id.with_context()。这将返回一个新版本的book_id(它是一个library.book记录集),并将键添加到当前context中。我们在这里加了一个键,
avoid_deactivate=True,但是如果需要,可以添加多个键。
在步骤6中,我们检查上下文对于avoid_deactivate键是否有正值。我们避免让书失效,这样即使书丢了,图书管理员也能看到。
这只是上下文的一个简单示例,但是您可以根据需求在ORM中的任何位置使用它。
有更多的…
还可以将字典传递给with_context()。在这种情况下,字典被用作新上下文,它将覆盖当前上下文。所以,第5步也可以写成:
new_context = self.env.context.copy()
new_context.update({\'avoid_deactivate\': True})
book_with_different_context = self.book_id.with_context(new_context)
book_with_different_context.make_lost()
使用with_context()涉及到创建一个新的环境实例。这个环境将有一个初始为空的记录集缓存,它将独立于self.env的缓存演化。这可能会导致虚假的数据库查询。在任何情况下,您都应该避免在循环内部创建新环境,并尝试将这些创建的环境移动到可能的最外层范围。
另请参阅
参考给出的内容,以学习更多关于上下文在Odoo:
在第6章基本服务器端开发中,为模型配制获取一个空记录集,解释了环境是什么。
在第10章后端视图中,向表单和动作传递参数-上下文配方,解释了如何修改动作定义中的上下文。
在第6章基本服务器端开发中,对记录的搜索解释了活动记录。
大多数时候,您可以使用search()方法执行所需的操作。然而,有时候,你需要更多;您要么不能使用域语法表达您想要的内容(对于这种语法,有些操作非常棘手,如果不是完全不可能的话),要么您的查询需要多次调用search(),这会导致效率低下。
本内容向您展示了如何使用原始SQL查询来获取用户借阅特定图书的平均天数。
准备
对于这个内容,我们将使用上一个内容中的my_library模块。为了简单起见,我们将只在日志中打印结果,但在实际场景中,需要在业务逻辑中使用查询结果。在第10章后端视图中,我们将在用户界面中显示此查询的结果。
怎么做呢?
要获得有关用户保存某一特定图书的平均天数的信息,您需要执行以下步骤:
1. 将average_book_职业()方法添加到library.book:
def average_book_occupation(self):
...
2. 在该方法中,编写以下SQL查询:
sql_query = """
SELECT
lb.name,
avg((EXTRACT(epoch from age(return_date, rent_date)) /86400))::int
FROM library_book_rent AS lbr
JOIN library_book as lb
ON lb.id = lbr.book_id
WHERE lbr.state = \'returned\'
GROUP BY lb.name;"""
3.执行查询:
self.env.cr.execute(sql_query)
4. 获取结果并记录:
result = self.env.cr.fetchall()
logger.info("Average book occupation: %s", result)
5. 在library_book模式的表单视图中添加一个按钮来触发我们的方法:
<button name="average_book_occupation" string="Log AverageOcc." type="object" />
不要忘记在这个文件中导入logging。然后,重新启动并更新my_library模块。
它是如何工作的…
在步骤2中,我们声明一个SQL SELECT查询。这将返回用户持有特定图书的平均天数。如果你在PostgreSQL CLI中运行这个查询,你会得到一个基于你的书数据的结果:
步骤3调用存储在self.env.cr中的数据库游标上的execute()方法。它将查询发送到PostgreSQL并执行它。
第5步使用游标的fetchall()方法检索查询所选择的行列表。这个方法返回一个行列表。在我的例子中,这是[(\'Odoo 12 Development Cookbook\', 33), (\'PostgreSQL 10管理
内容”,81)。从我们执行的查询的形式中,我们知道每行将恰好有两个值,第一个是名称,另一个是用户持有特定图书的平均天数。然后,我们简单地记录它。
有更多的…
self.env.cr中的对象是一个围绕psycopg2游标的瘦包装器。下面的方法是你会想要使用的大部分时间:
execute(query, params):这将执行SQL查询,查询中标记为%s的参数被params中的值替换,params是一个元组。
警告:永远不要自己进行替换,因为这会使代码容易受到SQL注入的攻击。
fetchone():它从数据库中返回一行,包装在元组中(即使查询只选择了一列)
fetchall():它以元组列表的形式返回数据库中的所有行
fetchalldict():这个函数返回数据库中的所有行,作为将列名映射到值的字典列表
在处理原始SQL查询时要非常小心:
您绕过了应用程序的所有安全性。确保使用检索的任何id列表调用search([(\'id\', \'in\', tuple(ids)]),以过滤出用户无法访问的记录。
您所做的任何修改都将绕过附加模块设置的约束,除了在数据库级别强制执行的NOT NULL、UNIQUE和FOREIGN KEY约束之外。对于任何计算字段重新计算触发器也是如此,因此最终可能会损坏数据库。
另请参阅
有关访问权限管理,请参阅第11章访问安全。
在使用抽象模型进行可重用模型特征配制的第5章,应用模型,模型。引入了TransientModel基类。这个类与常规模型有很多共同之处,只是临时模型的记录会在数据库中定期清理;因此,将其命名为transient。它们用于创建向导或对话框,这些向导或对话框由用户在用户界面中填写,通常用于对数据库的持久记录执行操作。
准备
对于这个内容,我们将使用上一个内容中的my_library模块。此内容将添加一个新的向导。有了这个向导,图书管理员将能够一次发行多本书。
怎么做呢?
按照给定的步骤添加一个新的向导来创建图书租赁记录:
1. 向模块中添加一个新的暂态模型,定义如下:
class LibraryRentWizard(models.TransientModel):
_name = \'library.rent.wizard\'
borrower_id = fields.Many2one(\'res.partner\',string=\'Borrower\')
book_ids = fields.Many2many(\'library.book\',string=\'Books\')
2. 添加在瞬态模型上执行操作的回调方法。将以下代码添加到LibraryRentWizard类:
def add_book_rents(self):
rentModel = self.env[\'library.book.rent\']
for wiz in self:
for book in wiz.book_ids:
rentModel.create({
\'borrower_id\': wiz.borrower_id.id,
\'book_id\': book.id
})
3.为模型创建一个表单视图。将以下视图定义添加到模块视图:
<record id=\'library_rent_wizard_form\' model=\'ir.ui.view\'>
<field name=\'name\'>library rent wizard form view</field>
<field name=\'model\'>library.rent.wizard</field>
<field name=\'arch\' type=\'xml\'>
<form string="Borrow books">
<sheet>
<group>
<field name=\'borrower_id\'/>
</group>
<group>
<field name=\'book_ids\'/>
</group>
</sheet>
<footer>
<button string=\'Rent\' type=\'object\' name=\'record_book_rents\' class=\'btn-primary\'/>
<button string=\'Cancel\' class=\'btnAdvanced default\' special=\'cancel\'/>
</footer>
</form>
</field>
</record>
4. 创建一个动作和一个菜单条目来显示向导。在模块菜单文件中添加以下声明:
<act_window id="action_wizard_rent_books" name="Give on Rent" res_model="library.rent.wizard" view_mode="form" target="new" />
<menuitem id="menu_wizard_rent_books" parent="library_base_menu" action="action_wizard_rent_books" sequence="20" />
它是如何工作的…
步骤1定义一个新模型。除了基类是TransientModel而不是Model之外,它与其他模型没有什么不同。TransientModel和Model共享一个通用的基类BaseModel,如果你检查Odoo的源代码,你会发现99%的工作都在BaseModel中,Model和TransientModel几乎都是空的。
TransientModel记录中唯一改变的事情如下:
- 记录会定期从数据库中删除,因此用于临时模型的表不会随着时间的推移而增大。
- 不能在TransientModel上定义访问规则。任何人都可以创建记录,但是只有创建了记录的用户才能读取和使用它。
- 不能在TransientModel中定义One2many字段,因为这会在持久模型上添加一个链接到瞬态数据的列。在这种情况下使用Many2many关系。你可以,
当然,为瞬态模型之间的关系定义Many2one和One2many字段。
我们在模型中定义了两个字段:一个用于存储成员借阅图书,另一个用于存储被借阅的图书列表。例如,我们可以添加其他标量字段来记录预定的返回日期。
步骤2将代码添加到向导类中,该向导类将在中定义按钮时被调用,点击第三步。这段代码从向导中读取值,并为每本书创建library.book.rent记录。
步骤3为向导定义了一个视图。有关详细信息,请参阅第10章后端视图中的文档样式表单配制。最重要的一点是页脚(footer)的按钮;type属性设置为\'object\',这意味着当用户单击按钮时,将调用由按钮的name属性指定的名称的方法。
步骤4确保在应用程序的菜单中有向导的入口点。我们在操作中使用target=\'new\',这样表单视图就会显示为当前表单上的对话框。有关详细信息,请参考第10章后端视图中的添加菜单项和窗口操作配制。
有更多的…
这里有一些提示来增强您的向导。
使用上下文计算默认值
我们呈现的向导要求用户在表单中填写成员的名称。我们可以使用web客户机的一个特性来节省一些输入。当一个动作被执行时,上下文会被一些值更新,这些值可以被向导使用:
key | value |
active_model | 这是与操作相关的模型的名称。这通常是屏幕上显示的模型。 |
active_id | 这表明单个记录是活动的,并提供该记录的ID。 |
active_ids | 如果选择了多条记录,这将是一个带有id的列表。当触发操作时,在树视图中选择了几个项目时,就会发生这种情况。在表单视图中,您获得[active_id]。 |
active_domain | 这是向导将在其上操作的附加域。 |
这些值可以用来计算模型的默认值,甚至可以直接在按钮调用的方法中使用。为了改进本内容中的示例,如果我们在res.partner模型的表单视图上显示一个按钮来启动向导,向导创建的上下文将包含{\'active_model\':\'res.partner\',\'active_id\': <partner id>}。在这种情况下,您可以定义member_id字段,以获得由以下方法计算的默认值:
def _default_member(self):
if self.context.get(\'active_model\') == \'res.partner\':
return self.context.get(\'active_id\', False)
向导和代码重用
在步骤2中,我们可以在向导中删除for循环,通过假设len(self)为1,我们可以在方法的开头添加self.ensure_one(),如下所示:
def add_book_rents(self):
self.ensure_one()
rentModel = self.env[\'library.book.rent\']
for book in self.book_ids:
rentModel.create({
\'borrower_id\': self.borrower_id.id,
\'book_id\': book.id
})
在方法的开头添加self.ensure_one()将确保self中的记录数只有一个。如果self中有多个记录,就会出现错误。
我们建议在配制中使用的版本,不过,因为这可以让我们从其他地方的代码重用向导创建记录的向导,让他们在一个记录集(指的是结合记录集这样的配方在第六章,基本的服务器端开发,如何做到这一点),然后调用add_book_rents记录集()。在这里,代码很简单,您实际上不需要通过所有这些麻烦来记录一些书被不同的成员借来了。然而,在一个Odoo实例中,一些操作要复杂得多,有一个向导来做正确的事情总是好的的事情。在使用这些向导时,请确保检查源代码,以查看上下文中对active_model/active_id/active_ids键的任何可能使用。如果是这种情况,则需要传递一个自定义上下文(引用使用修改的上下文配方调用方法)。
重定向用户
步骤2中的方法不返回任何内容。这将导致操作执行后向导对话框关闭。另一种可能是让方法返回一个包含ir.action字段的字典。在这种情况下,web客户机将处理操作,就像用户单击了一个菜单条目一样。可以使用在BaseModel类上定义的get_formview_action()方法来实现这一点。例如,如果我们想要显示刚刚借阅图书的成员的表单视图,我们可以编写如下内容:
def add_book_rents(self): rentModel = self.env[\'library.book.rent\'] for wiz in self: for book in wiz.book_ids: rentModel.create({ \'borrower_id\': wiz.borrower_id.id, \'book_id\': book.id }) members = self.mapped(\'borrower_id\') action = members.get_formview_action() if len(members .ids) > 1:
action[\'domain\'] = [(\'id\', \'in\', tuple(members.ids))]
action[\'view_mode\'] = \'tree,form\'
return action
这将构建一个从该向导借阅图书的成员列表(实际上,当从用户界面调用向导时,只有一个这样的成员),并创建一个动态操作,该操作显示具有指定id的成员。
用户重定向技术可用于创建一个向导,该向导有几个步骤需要依次执行。向导中的每个步骤都可以使用前面步骤的值。通过提供一个Next按钮,该按钮调用在向导中定义的方法,该方法更新向导中的一些字段,并返回一个动作,该动作将重新显示相同的更新向导,并为下一步做好准备。
当编写Odoo模型时,通常情况下一些领域是相互关联的。在第5章应用程序模型中,我们在向模型配制添加约束验证中了解了如何指定字段之间的约束。这个内容展示了一个稍微不同的概念。在这里,当用户界面中修改一个字段时,会调用onchange方法来更新web客户机中记录的其他字段的值,通常是在表单视图中。
我们将通过提供一个类似于在“编写向导”中定义的向导来说明这一点,但是它可以用于记录账簿返回值。在向导中设置成员时,图书列表将更新为该成员当前所借的图书。在我们演示TransientModel上的onchange方法时,这些特性在普通模型上也是可用的。
准备
对于这个内容,我们将使用来自“编写向导”的my_library模块来指导本章的用户内容。我们将创建一个向导来归还借来的书。我们将添加一个onchange方法,当图书管理员选择一个成员字段时,该方法将自动填充图书。
您还需要为向导定义以下暂态模型来准备工作:
class LibraryReturnWizard(models.TransientModel):
_name = \'library.return.wizard\'
borrower_id =fields.Many2one(\'res.partner\', string=\'Member\')
book_ids = fields.Many2many(\'library.book\', string=\'Books\')
def books_returns(self):
loan = self.env[\'library.book.rent\']
for rec in self:
loans = loan.search([(\'state\', \'=\', \'ongoing\'),
(\'book_id\', \'in\', rec.book_ids.ids),
(\'borrower_id\', \'=\', rec.borrower_id.id)]
)
for loan in loans:
loan.book_return()
最后,您需要为向导定义一个视图、一个操作和一个菜单项。
这些步骤将留给您作为练习来执行。
怎么做呢?
要在用户被更改时自动填充要返回的图书列表,您需要在LibraryReturnsWizard步骤中添加一个onchange方法,其定义如下:
@api.onchange(\'borrower_id\')
def onchange_member(self):
rentModel = self.env[\'library.book.rent\']
books_on_rent = rentModel.search(
[(\'state\', \'=\', \'ongoing\'),
(\'borrower_id\', \'=\', self.borrower_id.id)]
)
self.book_ids = books_on_rent.mapped(\'book_id\')
它是如何工作的…
onchange方法使用@api.onchange装饰器,它传递更改的字段的名称,从而触发对该方法的调用。在我们的例子中,我们说每当用户界面中修改了borrower_id时,都必须调用该方法。
在方法体中,我们搜索成员当前所借的图书,并使用属性赋值来更新向导的book_ids属性。
@api.onchange装饰器负责修改发送到web客户机的视图,以添加on_change属性
有更多的…
onchange方法的基本用途是在用户界面中更改其他字段时计算字段的新值,正如我们在内容中看到的那样。
在方法体内部,您可以访问记录的当前视图中显示的字段,但不必访问模型的所有字段。这是因为onchange方法可以在记录存储到数据库之前在用户界面中创建时被调用!在onchange方法中,self处于特殊状态,表示self.id不是整数,而是odoo.models.NewId的实例。在此之前,您不能在onchange方法中对数据库进行任何更改,因为用户最终可能会取消记录的创建,这将不会回滚onchange方法在编辑过程中所做的任何更改。要检查这一点,可以使用self.env.in_onchange()和self.env.in_draft();如果当前执行的上下文是onchange方法,则前者返回True;如果self尚未提交到数据库,则后者返回True
此外,onchange方法可以返回一个Python字典。本词典可以有以下关键字:
warning:值必须是另一个字典,它的标题键和消息键分别包含对话框的标题和内容,在onchange方法运行时显示。这有助于提醒用户注意不一致或潜在的问题。
domain:该值必须是另一个将字段名映射到域的字典。当您希望根据另一个字段的值更改One2many字段的域时,这非常有用。
例如,假设我们在library.book.rent模型中为expected_return_date设置了一个固定的值,并且我们希望在一个成员有一些书过期时显示一个警告。我们还希望将图书的选择限制为用户当前所借的图书。我们可以重写onchange方法,如下:
@api.onchange(\'member_id\')
def onchange_member(self):
rentModel = self.env[\'library.book.rent\']
books_on_rent = rentModel.search([(\'state\', \'=\', \'ongoing\'),
(\'borrower_id\', \'=\', self.borrower_id.id)])
self.book_ids = books_on_rent.mapped(\'book_id\')
result = {
\'domain\': {\'book_ids\': [(\'id\', \'in\', self.book_ids.ids)]}
}
late_domain = [(\'id\', \'in\', books_on_rent.ids),
(\'expected_return_date\', \'<\', fields.Date.today())]
late_books = books_on_rent.search(late_domain)
if late_books:
message = (\'Warn the member that the following \'\'books are late:\n\')
titles = late_books.mapped(\'book_id.name\')
result[\'warning\'] = {
\'title\': \'Late books\',
\'message\': message + \'\n\'.join(titles)
}
return result
在第6章基本服务器端开发中,创建新记录和更新记录集记录配制的值提到这些操作不会自动调用onchange方法。然而,在许多情况下,调用这些操作很重要,因为它们更新创建或更新的记录中的重要字段。当然,您可以自己完成所需的计算,但这并不总是可能的,因为onchange方法可以由安装在您不了解的实例上的第三方附加组件模块添加或修改。
此内容解释了如何在创建记录之前手动调用onchange方法来调用记录上的onchange方法。
准备
在更改执行操作配制的用户时,我们添加了一个Rent this book按钮,这样非图书管理员的用户就可以自己借书了。现在,我们希望对返回图书执行相同的操作,但不是编写返回图书的逻辑,而是使用在定义onchange方法配制中创建的图书返回向导。
怎么做呢?
在此内容中,我们将手动创建library.return.wizard模型的记录。 我们希望使用onchange方法为我们计算退回的书本。 为此,您需要执行以下步骤:
1. 在library.book模型中创建return_this_books方法:
@api.multi
def return_all_books(self):
self.ensure_one()
2. 为library.return.wizard获取一个空记录集:
wizard = self.env[\'library.return.wizard\']
3.准备创建一个新的向导记录的值。在这里,我们将使用当前用户的合作伙伴ID作为borrower_id,但如果你想把这个按钮放在res.parnter模型上,你可以使用self.id:
values = {
\'borrower_id\': self.env.user.partner_id.id,
}
4. 检索向导的onchange规范:
specs = wizard._onchange_spec()
5. 获取onchange方法的结果:
updates = wizard.onchange(values, [\'borrower_id\'], specs)
6. 将这些结果与新向导的值合并:
value = updates.get(\'value\', {})
for name, val in value.items():
if isinstance(val, tuple):
value[name] = val[0]
values.update(value)
7.创建向导:
wiz = wizard.create(values)
return wiz.sudo().books_returns()
完整代码
def return_all_books(self):
self.ensure_one()
# 为library.return.wizard获取一个空的记录集:
wizard = self.env[\'library.return.wizard\']
# with Form(wizard) as return_form:
# return_form.borrower_id = self.env.user.partner_id
# record = return_form.save()
# record.books_returns()
wizard.create({\'borrower_id\': self.env.user.partner_id.id}).books_returns()
# 准备创建一个新的向导记录的值。在这里,我们将使用当前用户的partner的ID作为borrower_id,
# 但如果你想把这个按钮放在res.parnter模型上,你可以使用self.id:
# values = {
# \'borrower_id\': self.env.user.partner_id.id,
# }
# 检索向导的onchange规范:
# 调用模型上的_onchange_spec方法,不传递参数。此方法将检索修改其他字段触发的更新。
# 它通过检查模型的表单视图(请记住,onchange方法通常由web客户机调用)来实现这一点。
# specs = wizard._onchange_spec()
# 获取onchange方法的结果:
# 调用模型的onchange(values, field_name, field_onchange)
# 方法,包含三个参数: #
# values: 我们想要在记录上设置的值列表。您需要为期望onchange方法修改的所有字段提供一个值。
# 由于这个原因,我们将book_ids设置为False。
#
# field_name: 我们想要触发onchange方法的字段列表。您可以传递一个空列表,ORM将使用值中定义的字段。
# 但是,您通常希望手动指定这个列表,以控制计算的顺序,以防不同的字段可以更新一个公共字段。
#
# field_onchange: 在一述中计算的onchange规范。该方法找出必须调用哪些onchange方法,
# 以及按什么顺序调用,并返回一个字典,其中可以包含以下内容
# value: 这是一个新计算的字段值的字典。这个字典只提供传递给onchange()的值参数中的键。
# 注意,Many2one字段被映射到一个元组,其中包含(id, display_name) 作为web客户端的优化。
# warning: 这是一个字典,其中包含web客户机将向用户显示的警告消息。
# domain: 这是一个将字段名称映射到新的有效域的字典。
# updates = wizard.onchange(values, [\'borrower_id\'], specs)
# 将这些结果与新向导的值合并:
# 使用onchange计算的值更新我们的初始值字典。我们处理与Many2one字段对应的值,只保留id。
# 为了做到这一点,我们利用了这样一个事实,即这些字段的值仅作为元组返回。
# value = updates.get(\'value\', {})
# for name, val in value.items():
# if isinstance(val, tuple):
# value[name] = val[0]
# values.update(value)
#
# # 创建向导:
# wiz = wizard.create(values)
# return wiz.sudo().books_returns()
# 如果您需要在修改字段后调用onchange方法,代码是相同的。您只需要为记录的值获取一个字典,可以在修改字段之后使用values = dict(record._cache)
# 获得该记录值。
# 如果您想了解更多关于创建和更新记录的信息,请参考第6章“基本服务器端开发”中的“创建新记录和更新记录集记录的值”。
#
它是如何工作的…
关于步骤1到步骤3的解释,请参考第6章基本服务器端开发中的创建新记录的方法。
步骤4调用模型上的_onchange_spec方法,不传递参数。此方法将检索修改其他字段触发的更新。它通过检查模型的表单视图(请记住,onchange方法通常由web客户机调用)来实现这一点。
步骤5调用模型的onchange(values, field_name, field_onchange)方法,包含三个参数:
values:我们要在记录上设置的值列表。您需要为期望onchange方法修改的所有字段提供一个值。由于这个原因,在菜谱中,我们将book_ids设置为False。
field_name:要为其触发onchange方法的字段列表。您可以传递一个空列表,ORM将使用值中定义的字段。但是,您通常希望手动指定此列表
控制计算的顺序,以防不同的字段可以更新一个公共字段。
field_onchange:在步骤4中计算的onchange规范。该方法找出必须调用哪些onchange方法,以及按什么顺序调用,并返回一个字典,其中可以包含以下内容Keys:
value:这是一个新计算字段值的字典。这个字典只提供传递给onchange()的值参数中的键。注意,Many2one字段映射到一个元组,该元组包含(id,display_name)作为web客户端的优化。
warning:这是一个字典,其中包含web客户端将向用户显示的警告消息。
domain:这是一个将字段名映射到新有效域的字典。
通常,当手动操作onchange方法时,我们只关心值是多少。
步骤6使用onchange计算的值更新我们的初始值字典。我们处理与Many2one字段对应的值,只保留id。为了做到这一点,我们利用了这样一个事实,即这些字段的值仅作为元组返回。
最后,第7步创建记录。
有更多的…
如果您需要在修改字段后调用onchange方法,代码是相同的。您只需要为记录的值获取一个字典,可以在修改字段之后使用values = dict(record._cache)获得该记录值。
另请参阅
如果您想了解更多关于创建和更新记录的信息,请参考第6章“基本服务器端开发”中的“创建新记录和更新记录集记录的值”。
在设计一个附加模块时,我们在类中建模数据,然后由Odoo映射到数据库表。我们应用一些众所周知的设计原则,例如关注点分离和数据规范化。但是,在模块设计的后期阶段,将来自多个模型的数据聚合到一个表中,并可能在此过程中对它们执行一些操作,特别是用于报告或生成仪表板时,这是很有用的。为了让这个过程更简单,也为了充分利用Odoo中底层PostgreSQL数据库引擎的强大功能,可以定义一个由PostgreSQL视图支持的只读模型,而不是一个表格。
在本章中,我们将重用“撰写向导”中的rent模型来指导用户的内容,并且我们将创建一个新模型,以便更容易地收集关于书籍和作者的统计信息。
准备
对于这个内容,我们将使用上一个内容中的my_library模块。我们将创建一个名为library.book.rent.statistics的新模型。用于保存统计数据的统计信息。
怎么做呢?
要创建一个支持PostgreSQL视图的新模型,请遵循以下说明:
1. 创建一个新模型,将_auto类属性设置为False:
class LibraryBookRentStatistics(models.Model):
_name = \'library.book.rent.statistics\'
_auto = False
2. 声明你想在模型中看到的字段,将它们设置为readonly:
book_id = fields.Many2one(\'library.book\', \'Book\', readonly=True)
rent_count = fields.Integer(string="Times borrowed",readonly=True)
average_occupation = fields.Integer(string="Average Occupation (DAYS)", readonly=True)
3.定义init()方法来创建视图:
@api.model_cr
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
query = """
CREATE OR REPLACE VIEW library_book_rent_statistics AS
(
SELECT min(lbr.id) as id, lbr.book_id as book_id, count(lbr.id) as rent_count,
avg((EXTRACT(epoch from age(return_date,rent_date)) / 86400))::int as average_occupation
FROM library_book_rent AS lbr
JOIN library_book as lb ON lb.id = lbr.book_id
WHERE lbr.state = \'returned\'
GROUP BY lbr.book_id
);
"""
self.env.cr.execute(query)
4. 现在可以为新模型定义视图。透视视图对于研究数据特别有用(参考第10章后端视图)。
5. 不要忘记为新模型定义一些访问规则(参见第11章,访问安全)。
它是如何工作的…
通常,Odoo将使用列的字段定义为您定义的模型创建一个新表。实际上,这是因为在BaseModel类中,_auto属性默认为True。在步骤1中,通过将这个类属性定位为False,我们告诉Odoo我们将自己管理这个属性。
在步骤2中,我们定义了一些字段,Odoo将使用这些字段生成一个表。我们小心地将它们标记为readonly=True,这样视图就不会启用那些你无法保存的修改,因为PostgreSQL视图是只读的。
步骤3定义了init()方法。这种方法通常不起作用;它在_auto_init()之后调用(它在_auto =True时负责创建表,但不执行其他操作),我们使用它创建一个新的SQL视图(或者在模块升级时更新现有视图)。视图创建查询必须创建一个具有与模型字段名称匹配的列名的视图。
在这种情况下,忘记在视图定义查询中重命名列是一个常见的错误,当Odoo无法找到列时,这将导致错误消息。
注意,我们还需要提供一个名为ID的整数列,它包含惟一的值。
有更多的…
在这些模型中也可以有一些计算的和相关的字段。惟一的限制是不能存储字段(因此,不能使用它们对记录进行分组或搜索)。但是,在前面的示例中,我们可以通过添加列来提供图书编辑器,列的定义如下:
publisher_id = fields.Many2one(\'res.partner\',related=\'book_id.publisher_id\', readonly=True)
如果需要按发布者进行分组,则需要通过在视图定义中添加字段来存储该字段,而不是使用相关字段。
在Odoo中,你可以通过设置选项提供可选功能。用户可以在任何时候启用或禁用此选项。我们将演示如何在此内容中创建设置选项。
准备
在以前的内容中,我们添加了一些按钮,以便非图书管理员的用户可以借阅和归还图书。但是,并不是每个库都是如此,因此我们将创建一个设置选项来启用和禁用该特性。我们将通过隐藏这些按钮来实现这一点。在这个内容中,我们将使用与上一个内容相同的my_library模块。
怎么做呢?
为了创建自定义设置选项,请遵循以下步骤:
1. 在my_library/security/groups.xml文件中添加一个新组:
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="group_librarian" model="res.groups">
<field name="name">图书管理员组</field>
<field name="users" eval="[(4, ref(\'base.user_admin\'))]"/>
</record>
<record id="group_self_borrow" model="res.groups">
<field name="name">个人借书</field>
<field name="users" eval="[(4, ref(\'base.user_admin\'))]"/>
</record>
</data>
</odoo>
2. 通过继承res.config.settings模型添加一个新字段:
# -*- coding: utf-8 -*-
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = \'res.config.settings\'
group_self_borrow = fields.Boolean(string="Self borrow", implied_group=\'my_library.group_self_borrow\')
3.用xpath在现有设置视图中添加这个字段(更多细节,请参阅第10章,后端视图):
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.library</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="Library" string="Library" data-key="my_library" groups="my_library.group_librarian">
<h2>Library</h2>
<div class="row mt16 o_settings_container">
<div class="col-12 col-lg-6 o_setting_box" id="library">
<div class="o_setting_left_pane">
<field name="group_self_borrow"/>
</div>
<div class="o_setting_right_pane">
<label for="group_self_borrow"/>
<div class="text-muted">
Allow users to borrow and return books by themself
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
4. 添加一些actions和一个menu为Settings:
<record id="library_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_library\'}</field>
</record>
<menuitem name="Settings" id="library_book_setting_menu"
parent="library_base_menu"
action="library_config_settings_action"
sequence="50"/>
5. 修改图书表单视图中的按钮并添加my_library.group_self_borrow组:
<button name="book_rent" string="Rent this book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
<button name="return_all_books" string="Return all book" type="object" class="btn-primary" groups="my_library.group_self_borrow"/>
它是如何工作的…
在Odoo中,所有设置选项都添加到res.config.settings模型中。res.config.settings是一个瞬态模型。
在步骤1中,我们创建了一个新的安全组。我们将使用这个组创建隐藏和显示按钮。
在第2步中,我们通过继承在res.config.settings模型中添加了一个新的布尔字段。我们添加了一个implied_group属性,其值为my_library.group_self_borrow。这个组将分配给所有odoo用户,当管理员启用或禁用布尔字段选项时。
Odoo设置使用一个表单视图显示设置选项在用户界面。所有这些选项都添加到带有外部ID base.res_config_settings_view_form的单个表单视图中。在步骤3中,我们通过继承这个设置表单视图在用户界面中添加了选项。我们使用xpath添加设置选项。在第10章“后端视图”中,我们将详细介绍这一点。在表单定义中,您将发现此选项的属性data-key值将是您的模块名。只有在设置中添加一个全新的标签时才需要。否则,您可以使用xpath在现有模块的Settings选项卡中添加选项。
在步骤4中,我们添加了一个操作和一个菜单来从用户界面访问配置选项。当单击菜单时,您需要从操作中传递{\'module\': \'my_library\'}上下文来打开my_library模块的设置选项卡。
在步骤5中,我们将my_library.group_self_borrow组添加到按钮中。由于存在这个组,借用和返回按钮将根据设置选项被隐藏或显示。
在此之后,您将看到该库的单独设置选项卡,并且在该选项卡中,您将看到一个布尔字段,用于启用或禁用自借选项。当您启用或禁用此选项时,在后台,Odoo将应用或删除implied_group给所有Odoo用户或从所有Odoo用户。因为我们在按钮上添加了组,所以如果用户有组,按钮将被显示,如果用户没有组,按钮将被隐藏。在第11章“访问安全”中,我们将详细讨论安全组。
有更多的…
还有一些其他的方法来管理设置选项。其中一种方法是分离新模块中的特性,并通过选项安装或卸载它们。为此,您需要添加一个布尔字段,该字段的模块名称以module_为前缀。例如,如果我们创建一个名为my_library_extras的新模块,则需要添加一个布尔字段,如下所示:
module_my_library_extras = fields.Boolean(string=\'Library Extra Features\')
当您启用或禁用这个选项,odoo将安装或卸载my_libarary_extras模块。
在第7章模块数据中,您看到了如何从XML或CSV文件中添加、更新和删除记录。然而,有时业务案例很复杂,不能使用数据文件来解决。在这种情况下,您可以使用清单文件中的init钩子来执行您想要的操作。
准备
我们将使用与上一个内容相同的my_library模块。为了简单起见,在本内容中,我们将通过post_init_hook创建一些图书记录。
怎么做呢?
为了添加post_init_hook,请遵循以下步骤:
1. 用post_init_hook键在__manifest__.py文件中注册这个钩子:
...
\'post_init_hook\': \'add_book_hook\',
...
2. 在剩余的__init__.py文件中添加add_book_hook()方法:
from odoo import api, fields, SUPERUSER_ID
def add_book_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
book_data1 = {\'name\': \'Book 1\', \'date_release\':
fields.Date.today()}
book_data2 = {\'name\': \'Book 2\', \'date_release\':
fields.Date.today()}
env[\'library.book\'].create([book_data1, book_data2])
它是如何工作的…
在第一步中,我们在清单文件中用add_book_hook值注册了post_init_hook。这意味着在模块安装完成后,Odoo将在__init__.py中查找add_book_hook方法。如果找到,则使用数据库游标和注册表调用该方法。
在第2步中,我们声明了add_book_hook()方法,它将在模块安装后被调用。我们从这个方法中创建了两条记录。在实际情况中,您可以在这里编写复杂的业务逻辑。在这个例子中,我们看了post_init_hook,但是Odoo支持另外两个钩子:
pre_init_hook:这个钩子将在您开始安装模块时被调用。它是post_init_hook的反义词;它将在安装当前模块之前被调用。
uninstall_hook:这个钩子将在卸载模块时被调用。这主要用于您的模块需要垃圾收集机制时。