实践环境
Odoo 14.0-20221212 (Community Edition)
需求描述
如下图(非实际项目界面截图,仅用于介绍本文主题),打开记录详情页(form视图),点击某个按钮(图中的”选取ffers”按钮),弹出一个向导(wizard)界面,并将详情页中内联tree视图(”Offers” Tab页)的列表记录展示到向导界面,且要支持复选框,用于选取目标记录,然执行目标操作。
详情页所属模型EstateProperty
class EstateProperty(models.Model):
_name = 'estate.property'
_description = 'estate property table'
# ... 略
offer_ids = fields.One2many("estate.property.offer", "property_id", string="PropertyOffer")
def action_do_something(self, args):
# do something
print(args)
Offers
Tab页Tree列表所属模型EstatePropertyOffer
class EstatePropertyOffer(models.Model):
_name = 'estate.property.offer'
_description = 'estate property offer'
# ... 略
property_id = fields.Many2one('estate.property', required=True)
代码实现
代码组织结构
为了更好的介绍本文主题,下文给出了项目文件大致组织结构(为了让大家看得更清楚,仅保留关键文件)
odoo14
├─custom
│ ├─estate
│ │ │ __init__.py
│ │ │ __manifest__.py
│ │ │
│ │ ├─models
│ │ │ estate_property.py
│ │ │ estate_property_offer.py
│ │ │ __init__.py
│ │ │
│ │ ├─security
│ │ │ ir.model.access.csv
│ │ │
│ │ ├─static
│ │ │ │
│ │ │ └─src
│ │ │ │
│ │ │ └─js
│ │ │ list_renderer.js
│ │ │
│ │ ├─views
│ │ │ estate_property_offer_views.xml
│ │ │ estate_property_views.xml
│ │ │ webclient_templates.xml
│ │ │
│ │ └─wizards
│ │ demo_wizard.py
│ │ demo_wizard_views.xml
│ │ __init__.py
│ │
├─odoo
│ │ api.py
│ │ exceptions.py
│ │ ...略
│ │ __init__.py
│ │
│ ├─addons
│ │ │ __init__.py
│ ...略
...略
wizard简介
wizard(向导)通过动态表单描述与用户(或对话框)的交互会话。向导只是一个继承TransientModel
而非model
的模型。TransientModel
类扩展Model
并重用其所有现有机制,具有以下特殊性:
-
wizard记录不是永久的;它们在一定时间后自动从数据库中删除。这就是为什么它们被称为瞬态(transient)。
-
wizard可以通过关系字段(
many2one
或many2many
)引用常规记录或wizard记录,但常规记录不能通过many2one
字段引用wizard记录
详细代码
注意:为了更清楚的表达本文主题,代码文件中部分代码已略去
wizard实现
odoo14customestatewizardsdemo_wizard.py
实现版本1
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import logging
from odoo import models,fields,api
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class DemoWizard(models.TransientModel):
_name = 'demo.wizard'
_description = 'demo wizard'
property_id = fields.Many2one('estate.property', string='property')
offer_ids = fields.One2many(related='property_id.offer_ids')
def action_confirm(self):
'''选中记录后,点击确认按钮,执行的操作'''
#### 根据需要对获取的数据做相应处理
# ... 获取数据,代码略(假设获取的数据存放在 data 变量中)
record_ids = []
for id, value_dict in data.items():
record_ids.append(value_dict.get('data', {}).get('id'))
if not record_ids:
raise UserError('请选择记录')
self.property_id.action_do_something(record_ids)
return True
@api.model
def action_select_records_via_checkbox(self, args):
'''通过wizard窗口界面复选框选取记录时触发的操作
@params: args 为字典
'''
# ...存储收到的数据(假设仅存储data部分的数据),代码略
return True # 注意,执行成功则需要配合前端实现,返回True
@api.model
def default_get(self, fields_list):
'''获取wizard 窗口界面默认值,包括记录列表 #因为使用了@api.model修饰符,self为空记录集,所以不能通过self.fieldName = value 的方式赋值'''
res = super(DemoWizard, self).default_get(fields_list)
record_ids = self.env.context.get('active_ids') # 获取当前记录ID列表(当前记录详情页所属记录ID列表) # self.env.context.get('active_id') # 获取当前记录ID
property = self.env['estate.property'].browse(record_ids)
res['property_id'] = property.id
offer_ids = property.offer_ids.mapped('id')
res['offer_ids'] = [(6, 0, offer_ids)]
return res
说明:
-
注意,不能使用类属性来接收数据,因为类属性供所有对象共享,会相互影响,数据错乱。
-
action_select_records_via_checkbox
函数接收的args
参数,其类型为字典,形如以下,其中f412cde5-1e5b-408c-8fc0-1841b9f9e4de
为UUID,供web端使用,用于区分不同页面操作的数据,'estate.property.offer_3'
为供web端使用的记录ID,'data'
键值代表记录的数据,其id
键值代表记录在数据库中的主键id,context
键值代表记录的上下文。arg
数据格式为:{'uuid':{'recordID1':{'data': {}, 'context':{}}, 'recordID2': {'data': {}, 'context':{}}}}
{'f412cde5-1e5b-408c-8fc0-1841b9f9e4de': {'estate.property.offer_3': {'data': {'price': 30000, 'partner_id': {'context': {}, 'count': 0, 'data': {'display_name': 'Azure Interior, Brandon Freeman', 'id': 26}, 'domain': [], 'fields': {'display_name': {'type': 'char'}, 'id': {'type': 'integer'}}, 'id': 'res.partner_4', 'limit': 1, 'model': 'res.partner', 'offset': -1, 'ref': 26, 'res_ids': [], 'specialData': {}, 'type': 'record', 'res_id': 26}, 'validity': 7, 'date_deadline': '2022-12-30', 'status': 'Accepted', 'id': 21}, 'context': {'lang': 'en_US', 'tz': 'Europe/Brussels', 'uid': 2, 'allowed_company_ids': [1], 'params': {'action': 85, 'cids': 1, 'id': 41, 'menu_id': 70, 'model': 'estate.property', 'view_type': 'form'}, 'active_model': 'estate.property', 'active_id': 41, 'active_ids': [41], 'property_pk_id': 41}}}}
实现版本2
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import uuid
import logging
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError, MissingError
_logger = logging.getLogger(__name__)
class DemoWizard(models.TransientModel):
_name = 'demo.wizard'
_description = 'demo wizard'
property_id = fields.Many2one('estate.property', string='property')
property_pk_id = fields.Integer(related='property_id.id') # 用于action_confirm中获取property
offer_ids = fields.One2many(related='property_id.offer_ids')
@api.model
def action_confirm(self, data:dict):
'''选中记录后,点击确认按钮,执行的操作'''
#### 根据需要对获取的数据做相应处理
record_ids = []
for id, value_dict in data.items():
record_ids.append(value_dict.get('data', {}).get('id'))
if not record_ids:
raise UserError('请选择记录')
property_pk_id = None
for id, value_dict in data.items():
property_pk_id = value_dict.get('context', {}).get('property_pk_id')
break
if not property_pk_id:
raise ValidationError('do something fail')
property = self.env['estate.property'].browse([property_pk_id]) # 注意,,所以,这里不能再通过self.property_id获取了
if property.exists():
property.action_do_something(record_ids)
else:
raise MissingError('do something fail:当前property记录(id=%s)不存在' % property_pk_id)
return True
@api.model
def default_get(self, fields_list):
'''获取wizard 窗口界面默认值,包括记录列表'''
res = super(DemoWizard, self).default_get(fields_list)
record_ids = self.env.context.get('active_ids')
property = self.env['estate.property'].browse(record_ids)
res['property_id'] = property.id
res['property_pk_id'] = property.id
offer_ids = property.offer_ids.mapped('id')
res['offer_ids'] = [(6, 0, offer_ids)]
return res
odoo14customestatewizards__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import demo_wizard
odoo14customestate__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import models
from . import wizards
odoo14customestatewizardsdemo_wizard_views.xml
实现版本1
对应demo_wizard.py
实现版本1
demo.wizard.form
demo.wizard
选取offers
demo.wizard
ir.actions.act_window
form
new
说明:
-
hasCheckBoxes
设置"true"
,则显示复选框。以下属性皆在hasCheckBoxes
为"true"
的情况下起作用。 -
modelName
点击列表复选框时,需要访问的模型名称,需要配合modelMethod
方法使用,缺一不可。可选 -
modelMethod
点击列表复选框时,需要调用的模型方法,通过该方法收集列表勾选记录的数据。可选。 -
jsMethodOnModelMethodDone
定义modelMethod
方法执行完成后,需要调用的javascript方法(注意,包括参数,如果没有参数则写成()
,形如jsMethod()
)。可选。 -
jsMethodOnToggleCheckbox
定义点击列表复选框时需要调用的javascript方法,比modelMethod
优先执行(注意,包括参数,如果没有参数则写成()
,形如jsMethod()
)。可选。
以上参数同下文saveSelectionsToSessionStorage
参数可同时共存
如果需要将action绑定到指定模型指定视图的Action,可以在ir.actions.act_window
定义中添加binding_model_id
和binding_view_types
字段,如下:
选取offers
demo.wizard
ir.actions.act_window
form
new
form
效果如下
参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/actions.html
实现版本2
对应demo_wizard.py
实现版本2
demo.wizard.form
demo.wizard
选取offers
demo.wizard
ir.actions.act_window
form
new
说明:
-
saveSelectionsToSessionStorage
为"true"
则表示点击复选框时,将当前选取的记录存到浏览器sessionStorage
中,可选
odoo14customestatesecurityir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
# ...略
access_demo_wizard_model,access_demo_wizard_model,model_demo_wizard,base.group_user,1,1,1,1
注意:wizard
模型也是需要添加模型访问权限配置的
复选框及勾选数据获取实现
大致思路通过继承web.ListRenderer
实现自定义ListRenderer,进而实现复选框展示及勾选数据获取。
odoo14customestatestaticsrcjslist_renderer.js
注意:之所以将uuid
函数定义在list_renderer.js
中,是为了避免因为js顺序加载问题,可能导致加载list_renderer.js
时找不到uuid
函数定义问题。
function uuid() {
var s = [];
var hexDigits = "0123456789abcdef";
for (var i = 0; i
实践过程中,有尝试过以下实现方案,视图通过指定相同服务ID web.ListRenderer
来覆盖框架自带的web.ListRenderer
定义,这种实现方案只能在非Debug
模式下正常工作,且会导致无法开启Debug
模式,odoo.define
实现中会对服务是否重复定义做判断,如果重复定义则会抛出JavaScript异常。
odoo.define('web.ListRenderer', function (require) {
"use strict";
//...略,同上述代码
// odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer;
return ListRenderer;
});
笔者后面发现,可以使用include
替代extend
方法修改现有的web.ListRenderer
,如下
odoo.define('estate.ListRenderer', function (require) {
"use strict";
var ListRenderer = require('web.ListRenderer');
ListRenderer = ListRenderer.include({//...略,同上述代码});
// odoo.__DEBUG__['services']['web.ListRenderer'] = ListRenderer; //不需要添加这行代码了
});
odoo14customestatestaticsrcjsdemo_wizard_views.js
实现版本1
供demo_wizard_views.xml
实现版本1使用
function disableActionConfirmButton(){ // 禁用按钮
$("button[name='action_confirm']").attr("disabled", true);
}
function enableActionConfirmButton(){ // 启用按钮
$("button[name='action_confirm']").attr("disabled", false);
}
这里的设计是,执行复选框操作时,先禁用按钮,不允许执行确认操作,因为执行复选框触发的请求可能没那么快执行完成,前端数据可能没完全传递给后端,此时去执行操作,可能会导致预期之外的结果。所以,等请求完成再启用按钮。
实现版本2
供demo_wizard_views.xml
实现版本2使用
function do_confirm_action(modelName, modelMethod, context){
$("button[name='action_confirm']").attr("disabled", true); // 点击按钮后,禁用按钮状态,比较重复点击导致重复发送请求
var wizard_dialog = $(event.currentTarget.offsetParent.parentElement.parentElement);
var dataUUID = $(event.currentTarget.parentElement.parentElement.parentElement.parentElement).find('div.o_list_view').prop('id');
var rpc = odoo.__DEBUG__.services['web.rpc'];
rpc.query({
model: modelName,
method: modelMethod,
args: [JSON.parse(window.sessionStorage.getItem(dataUUID) || '{}')]
}).then(function (res) if (res == true) {
wizard_dialog.css('display', 'none'); // 隐藏对话框
window.sessionStorage.removeItem(dataUUID);
} else {
$("button[name='action_confirm']").attr("disabled", false);
}
}).catch(function (err) {
$("button[name='action_confirm']").attr("disabled", false);
});
}
odoo14odooaddonsbaserngtree_view.rng
可选操作。如果希望hasCheckBoxes
,modelName
,modelMethod
等也可作用于非内联tree视图,则需要编辑该文件,添加hasCheckBoxes
,modelName
,modelMethod
等属性,否则,更新应用的时候会报错。