• odoo wizard界面显示带复选框列表及勾选数据获取


    实践环境

    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 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
    
    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 models
    from . import wizards
    
    odoo14\custom\estate\wizards\demo_wizard_views.xml
    实现版本1

    对应demo_wizard.py实现版本1

    
    <odoo>
        <data>
            <record id="demo_wizard_view_form" model="ir.ui.view">
                <field name="name">demo.wizard.formfield>
                <field name="model">demo.wizardfield>
                <field name="arch" type="xml">
                    <form>
                        <field name="offer_ids">
                            <tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
                                <field name="price" string="Price"/>
                                <field name="partner_id" string="partner ID"/>
                                <field name="validity" string="Validity(days)"/>
                                <field name="date_deadline" string="Deadline"/>
                                <button name="action_accept_offer" string=""  type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                                <button name="action_refuse_offer" string=""  type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                                <field name="status" string="Status"/>
                            tree>
                        field>
                        <footer>
                            <button name="action_confirm" type="object" string="确认(do something you want)" class="oe_highlight"/>
                            <button string="取消" class="oe_link" special="cancel"/>
                        footer>
                    form>
                field>
            record>
            
            <record id="action_demo_wizard" model="ir.actions.act_window">
                <field name="name">选取offersfield>
                <field name="res_model">demo.wizardfield>
                <field name="type">ir.actions.act_windowfield>
                <field name="view_mode">formfield>
                <field name="target">newfield>            
            record>
        data>
    odoo>
    

    说明:

    <tree hasCheckBoxes="true" modelName="demo.wizard" modelMethod="action_select_records_via_checkbox" jsMethodOnModelMethodDone="enableActionConfirmButton()" jsMethodOnToggleCheckbox="disableActionConfirmButton()">
    
    • 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字段,如下:

            <record id="action_demo_wizard" model="ir.actions.act_window">
                <field name="name">选取offersfield>
                <field name="res_model">demo.wizardfield>
                <field name="type">ir.actions.act_windowfield>
                <field name="view_mode">formfield>
                <field name="target">newfield>            
                
                <field name="binding_model_id" ref="estate.model_estate_property"/>
                <field name="binding_view_types">formfield>
            record>
    

    效果如下

    参考连接:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/addons/actions.html

    实现版本2

    对应demo_wizard.py实现版本2

    
    <odoo>
        <data>
            <record id="demo_wizard_view_form" model="ir.ui.view">
                <field name="name">demo.wizard.formfield>
                <field name="model">demo.wizardfield>
                <field name="arch" type="xml">
                    <form>
                        <field name="property_pk_id" invisible="1"/>
                        <field name="offer_ids" context="{'property_pk_id': property_pk_id}">
                            <tree string="List" hasCheckBoxes="true" saveSelectionsToSessionStorage="true">
                                <field name="price" string="Price"/>
                                <field name="partner_id" string="partner ID"/>
                                <field name="validity" string="Validity(days)"/>
                                <field name="date_deadline" string="Deadline"/>
                                <button name="action_accept_offer" string=""  type="object" icon="fa-check" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                                <button name="action_refuse_offer" string=""  type="object" icon="fa-times" attrs="{'invisible': [('status', 'in', ['Accepted','Refused'])]}"/>
                                <field name="status" string="Status"/>
                            tree>
                        field>
                        <footer>
                            <button name="action_confirm" onclick="do_confirm_action('demo.wizard','action_confirm')"  string="确认(do something you want)" class="oe_highlight"/>
                            <button string="取消" class="oe_link" special="cancel"/>
                        footer>
                    form>
                field>
            record>
            
            <record id="action_demo_wizard" model="ir.actions.act_window">
                <field name="name">选取offersfield>
                <field name="res_model">demo.wizardfield>
                <field name="type">ir.actions.act_windowfield>
                <field name="view_mode">formfield>
                <field name="target">newfield>            
            record>
        data>
    odoo>
    

    说明:

    • 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 0010
    	s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
    	s[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等属性,否则,更新应用的时候会报错。

    
    <rng:grammar xmlns:rng="http://relaxng.org/ns/structure/1.0"
                 xmlns:a="http://relaxng.org/ns/annotation/1.0"
                 datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
        
        <rng:define name="tree">
            <rng:element name="tree">
                
                <rng:optional><rng:attribute name="decoration-warning"/>rng:optional>
                <rng:optional><rng:attribute name="banner_route"/>rng:optional>
                <rng:optional><rng:attribute name="sample"/>rng:optional>
                
            rng:element>
        rng:define>
        
    rng:grammar>
    
    odoo14\custom\estate\views\webclient_templates.xml

    用于加载自定义js

    
    <odoo>
        <template id="assets_common" inherit_id="web.assets_common" name="Backend Assets (used in backend interface)">
             <xpath expr="//script[last()]" position="after">
                 <script type="text/javascript" src="/estate/static/src/js/list_renderer.js">script>
                 <script type="text/javascript" src="/estate/static/src/js/demo_wizard_views.js">script>
             xpath>
        template>
    odoo>
    
    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
    
    <odoo>
        
        <record id="estate_property_view_form" model="ir.ui.view">
            <field name="name">estate.property.formfield>
            <field name="model">estate.propertyfield>
            <field name="arch" type="xml">
                <form string="estate property form">
                    <header>
                         <button name="%(action_demo_wizard)d"
                                    type="action"
                                    string="选取offers" class="oe_highlight"/>
                        
                    header>
                    <sheet>
                                            
                        <notebook>
                                                    
                            <page string="Offers">
                                <field name="offer_ids" attrs="{'readonly': [('state', 'in', ['Offer Accepted','Sold','Canceled'])]}"/>
                            page>
                                 
                        notebook>
                    sheet>
                form>
            field>
        record>    
    odoo>
    

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

    参考连接

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

  • 相关阅读:
    AcWing 800. 数组元素的目标和——算法基础课题解
    始祖双碳新闻 | 2022年8月15日碳中和行业早知道
    请做好3年内随时失业的准备?
    Data Augmentation techniques in time series domain: A survey and taxonomy
    使用docker-compose私有化部署 GitLab
    怎么利用互联网赚钱,网上赚钱的7种方法
    Oracle创建索引的LOGGING | NOLOGGING区别
    Linux 线程同步、互斥锁、避免死锁、条件变量
    【C语言】从零开始理解初级指针
    Angular 14 inject 函数使用过程中的一些注意事项
  • 原文地址:https://www.cnblogs.com/shouke/p/17135887.html