• 搜索
    搜新闻
  • 您的位置: 首页 >  快讯

    天天最资讯丨odoo wizard界面显示带复选框列表及勾选数据获取

    博客园来源:2023-04-22 14:45:34
    实践环境

    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)

    OffersTab页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可以通过关系字段(many2onemany2many)引用常规记录或wizard记录,但常规记录不能通过many2one字段引用wizard记录

    详细代码

    注意:为了更清楚的表达本文主题,代码文件中部分代码已略去

    wizard实现odoo14\custom\estate\wizards\demo_wizard.py实现版本1
    #!/usr/bin/env python# -*- coding:utf-8 -*-import loggingfrom odoo import models,fields,apifrom 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 uuidimport loggingfrom odoo import models, fields, apifrom 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
    odoo14\custom\estate\wizards\__init__.py
    #!/usr/bin/env python# -*- coding:utf-8 -*-from . import demo_wizard
    odoo14\custom\estate\__init__.py
    #!/usr/bin/env python# -*- coding:utf-8 -*-from . import modelsfrom . import wizards
    odoo14\custom\estate\wizards\demo_wizard_views.xml实现版本1

    对应demo_wizard.py实现版本1

                            demo.wizard.form            demo.wizard                            

    说明:

    hasCheckBoxes设置"true",则显示复选框。以下属性皆在hasCheckBoxes"true"的情况下起作用。modelName点击列表复选框时,需要访问的模型名称,需要配合modelMethod方法使用,缺一不可。可选modelMethod点击列表复选框时,需要调用的模型方法,通过该方法收集列表勾选记录的数据。可选。jsMethodOnModelMethodDone定义modelMethod方法执行完成后,需要调用的javascript方法(注意,包括参数,如果没有参数则写成(),形如 jsMethod())。可选。jsMethodOnToggleCheckbox定义点击列表复选框时需要调用的javascript方法,比modelMethod优先执行(注意,包括参数,如果没有参数则写成(),形如 jsMethod())。可选。

    以上参数同下文saveSelectionsToSessionStorage参数可同时共存

    如果需要将action绑定到指定模型指定视图的Action,可以在ir.actions.act_window定义中添加binding_model_idbinding_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                            

    说明:

    saveSelectionsToSessionStorage"true"则表示点击复选框时,将当前选取的记录存到浏览器sessionStorage中,可选odoo14\custom\estate\security\ir.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,进而实现复选框展示及勾选数据获取。

    odoo14\custom\estate\static\src\js\list_renderer.js

    注意:之所以将uuid函数定义在list_renderer.js中,是为了避免因为js顺序加载问题,可能导致加载list_renderer.js时找不到uuid函数定义问题。

    function uuid() {var s = [];var hexDigits = "0123456789abcdef";for (var i = 0; i < 36; i++) {s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);}s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01s[8] = s[13] = s[18] = s[23] = "-";var uuid = s.join("");return uuid;}odoo.define("estate.ListRenderer", function (require) {    "use strict"; var ListRenderer = require("web.ListRenderer");ListRenderer = ListRenderer.extend({    init: function (parent, state, params) {    this._super.apply(this, arguments);    this.hasCheckBoxes = false;if ("hasCheckBoxes" in params.arch.attrs && params.arch.attrs["hasCheckBoxes"]) {                this.objectID = uuid();                $(this).attr("id", this.objectID);    this.hasCheckBoxes = true;    this.hasSelectors = true;    this.records = {}; // 存放当前界面记录    this.recordsSelected = {}; // 存放选取的记录    this.modelName = undefined; // 定义点击列表复选框时需要访问的模型    this.modelMethod = undefined; // 定义点击列表复选框时需要调用的模型方法    this.jsMethodOnModelMethodDone = undefined; // 定义modelMethod方法执行完成后,需要调用的javascript方法    this.jsMethodOnToggleCheckbox = undefined; // 定义点击列表复选框时需要调用的javascript方法,比modelMethod优先执行    if ("modelName" in params.arch.attrs && params.arch.attrs["modelName"]) {        this.modelName = params.arch.attrs["modelName"];    }    if ("modelMethod" in params.arch.attrs && params.arch.attrs["modelMethod"]) {        this.modelMethod = params.arch.attrs["modelMethod"];    }    if ("jsMethodOnModelMethodDone" in params.arch.attrs && params.arch.attrs["jsMethodOnModelMethodDone"]){        this.jsMethodOnModelMethodDone = params.arch.attrs["jsMethodOnModelMethodDone"];    }    if ("jsMethodOnToggleCheckbox" in params.arch.attrs && params.arch.attrs["jsMethodOnToggleCheckbox"]) {        this.jsMethodOnToggleCheckbox = params.arch.attrs["jsMethodOnToggleCheckbox"];    }                                if ("saveSelectionsToSessionStorage" in params.arch.attrs && params.arch.attrs["saveSelectionsToSessionStorage"]) {        this.saveSelectionsToSessionStorage = params.arch.attrs["saveSelectionsToSessionStorage"];    }            }},//_onToggleSelection: function (ev) {            // 点击列表表头的全选/取消全选复选框时会调用该函数//    this._super.apply(this, arguments);//        },        _onToggleCheckbox: function (ev) {            if (this.hasCheckBoxes) {                var classOfEvTarget = $(ev.target).attr("class");                /* cstom-control-input 刚好点中复选框input,                custom-control custom-checkbox 刚好点中复选框input的父元素div                o_list_record_selector 点击到复选框外上述div的父元素*/                                if (["custom-control custom-checkbox", "custom-control-input", "o_list_record_selector"].includes(classOfEvTarget)){                    if (this.jsMethodOnToggleCheckbox) {                        eval(this.jsMethodOnToggleCheckbox)                    }                    var id = $(ev.currentTarget).closest("tr").data("id"); // "custom-control-input" == classOfEvTarget                    var checked = !this.$(ev.currentTarget).find("input").prop("checked") // 获取复选框是否框选 "custom-control-input" != classOfEvTarget                    if ("custom-control-input" ==  classOfEvTarget) {                        checked = this.$(ev.currentTarget).find("input").prop("checked")                    }                                        if (id == undefined) {                        if (checked == true) { // 全选                            this.recordsSelected = JSON.parse(JSON.stringify(this.records));                        } else { // 取消全选                            this.recordsSelected = {};                        }                    } else {                        if (checked == true) { // 勾选单条记录                            this.recordsSelected[id] = this.records[id];                        } else { // 取消勾选单条记录                            delete this.recordsSelected[id];                        }                    }                    if (this.saveSelectionsToSessionStorage) {                        window.sessionStorage[this.objectID] = JSON.stringify(this.recordsSelected);                    }                                        // 通过rpc请求模型方法,用于传输界面勾选的记录数据                    if (this.modelName && this.modelMethod) {                        self = this;                        this._rpc({                                model: this.modelName,                                method: this.modelMethod,                                args: [this.recordsSelected],                            }).then(function (res) {                                if (self.jsMethodOnModelMethodDone) {                                    eval(self.jsMethodOnModelMethodDone);                                }                            });                    }                }            }            this._super.apply(this, arguments);        },        _renderRow: function (record) {            // 打开列表页时会渲染行,此时存储渲染的记录            if (this.hasCheckBoxes) {                this.records[record.id] = {"data": record.data, "context": record.context};            }            return this._super.apply(this, arguments);        }});odoo.__DEBUG__["services"]["web.ListRenderer"] = ListRenderer; //覆盖原有的ListRender服务});

    实践过程中,有尝试过以下实现方案,视图通过指定相同服务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;  //不需要添加这行代码了});
    odoo14\custom\estate\static\src\js\demo_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);    });}
    odoo14\odoo\addons\base\rng\tree_view.rng

    可选操作。如果希望hasCheckBoxesmodelNamemodelMethod等也可作用于非内联tree视图,则需要编辑该文件,添加hasCheckBoxesmodelNamemodelMethod等属性,否则,更新应用的时候会报错。

                                                                                                
    odoo14\custom\estate\views\webclient_templates.xml

    用于加载自定义js

        
    odoo14\custom\estate\__manifest__.py

    加载自定义模板文件,进而实现自定义js文件的加载

    #!/usr/bin/env python# -*- coding:utf-8 -*-{    "name": "estate",    "depends": ["base"],    "data":[        "views/webclient_templates.xml",        "security/ir.model.access.csv",        #...略        "wizards/demo_wizard_views.xml"        "views/estate_property_views.xml",        "views/estate_property_offer_views.xml",     ]}
    记录详情页视图实现odoo14\custom\estate\views\estate_property_views.xml
                    estate.property.form        estate.property                    

    说明:class="oe_highlight"设置按钮高亮显示

    参考连接

    https://blog.csdn.net/CBGCampus/article/details/128196983

    关键词:

    下一篇:
    上一篇: