You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Frappe框架:基于子表动态生成表格并实现数据持久化

在Frappe框架中实现动态表格数据持久化到数据库的解决方案

一、先搞定后台数据结构配置

你当前的问题核心是没有为动态表格配置合适的存储结构,直接用普通字段存JSON无法被Frappe正确持久化。需要创建子表Doctype来存储每一行数据:

  1. 新建子表Doctype,命名为Order Sheet Row:

    • 勾选「Is Child Table」选项
    • 添加以下字段:
      字段名类型标签(Label)
      po_noDataPO NO
      storeDataStore
      col_cdDataCol. Cd
      col_nameDataCol. Name
      qtyFloatQty
      delivery_dateDateDelivery Date
      intercomesDataIntercomes
      forex_rateFloatForex Rate
      size_dataJSONSize Data
  2. 在主Doctype Order Sheet中添加子表字段:

    • 字段名:order_sheet_rows
    • 类型:Table
    • 选项(Options):Order Sheet Row

二、修正前端JS代码逻辑

你现有的代码只生成了前端UI,但没有正确关联到Frappe的子表数据结构,且存在属性名错误(比如data-size未设置)。以下是修正后的完整代码:

frappe.ui.form.on('Order Sheet', {
    refresh: function(frm) {
        var htmlField = frm.fields_dict['size_chart'];
        htmlField.$wrapper.empty();

        // 固定列配置(对应子表字段名)
        var fixedColumns = [
            { label: 'PO NO', field: 'po_no' },
            { label: 'Store', field: 'store' },
            { label: 'Col. Cd', field: 'col_cd' },
            { label: 'Col. Name', field: 'col_name' },
            { label: 'Qty', field: 'qty' },
            { label: 'Delivery Date', field: 'delivery_date' },
            { label: 'Intercomes', field: 'intercomes' },
            { label: 'Forex Rate', field: 'forex_rate' }
        ];

        // 获取Size子表的动态列值
        var sizeValues = frm.doc.size.map(row => row.size).filter(Boolean);
        var allColumns = [...fixedColumns, ...sizeValues.map(size => ({ label: size, field: size }))];

        if (allColumns.length > 0) {
            // 创建表格容器
            var tableContainer = document.createElement('div');
            tableContainer.style.overflow = 'auto';
            tableContainer.style.maxHeight = '300px';

            var htmlTable = document.createElement('table');
            htmlTable.setAttribute('border', '1');
            htmlTable.style.width = '100%';

            // 生成表头
            var thead = document.createElement('thead');
            var headerRow = document.createElement('tr');
            allColumns.forEach(col => {
                var th = document.createElement('th');
                th.textContent = col.label;
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);
            htmlTable.appendChild(thead);

            // 生成表体(含已有数据回填)
            var tbody = document.createElement('tbody');
            // 加载已保存的子表数据
            if (frm.doc.order_sheet_rows && frm.doc.order_sheet_rows.length > 0) {
                frm.doc.order_sheet_rows.forEach((row, idx) => {
                    addRowToTable(tbody, idx, allColumns, row, frm);
                });
            } else {
                // 初始化3行空数据
                for (let i = 0; i < 3; i++) {
                    addRowToTable(tbody, i, allColumns, null, frm);
                }
            }
            htmlTable.appendChild(tbody);
            tableContainer.appendChild(htmlTable);
            htmlField.$wrapper.append(tableContainer);

            // 添加新增行按钮
            var addButton = document.createElement('button');
            addButton.textContent = 'Add Row';
            addButton.style.marginTop = '10px';
            addButton.addEventListener('click', function() {
                addRowToTable(tbody, tbody.childElementCount, allColumns, null, frm);
            });
            htmlField.$wrapper.append(addButton);
        }
    }
});

// 生成单一行(含数据回填和事件绑定)
function addRowToTable(tbody, rowIndex, columns, savedRow, frm) {
    var row = document.createElement('tr');
    // 初始化子表行数据
    if (!savedRow) {
        savedRow = frappe.model.add_child(frm.doc, 'Order Sheet Row', 'order_sheet_rows');
        // 初始化size_data为空对象
        savedRow.size_data = {};
    }

    columns.forEach(col => {
        var cell = document.createElement('td');
        var input = document.createElement('input');
        input.setAttribute('type', col.field === 'delivery_date' ? 'date' : 'text');
        input.setAttribute('data-row-index', rowIndex);
        input.setAttribute('data-field', col.field);
        input.setAttribute('data-is-size', sizeValues.includes(col.label) ? 'true' : 'false');
        
        // 回填已保存的数据
        if (savedRow) {
            if (sizeValues.includes(col.label)) {
                input.value = savedRow.size_data[col.label] || '';
            } else {
                input.value = savedRow[col.field] || '';
            }
        }

        // 绑定输入变更事件,同步到子表数据
        input.addEventListener('change', function() {
            updateRowData(frm, this, savedRow);
        });
        cell.appendChild(input);
        row.appendChild(cell);
    });
    tbody.appendChild(row);
}

// 更新子表行数据到frm.doc
function updateRowData(frm, inputElement, savedRow) {
    var field = inputElement.getAttribute('data-field');
    var isSize = inputElement.getAttribute('data-is-size') === 'true';
    var value = inputElement.value;

    if (isSize) {
        // 动态size列存入size_data JSON字段
        savedRow.size_data[field] = value;
    } else {
        // 固定列直接更新子表字段
        savedRow[field] = value;
    }
    // 刷新子表字段,触发Frappe的数据同步
    frm.refresh_field('order_sheet_rows');
}

三、关键逻辑说明

  1. 子表数据关联:通过frappe.model.add_child创建子表行,所有输入变更直接修改子表行对象,Frappe会自动处理保存、更新、删除逻辑
  2. 动态列存储:把动态size列的键值对存在子表的size_data JSON字段中,避免提前创建大量动态字段
  3. 数据回填:页面刷新时从frm.doc.order_sheet_rows读取已保存的数据,回填到动态表格中
  4. 字段类型适配:针对日期字段(如Delivery Date)使用date类型输入框,提升用户体验

四、验证保存效果

完成配置和代码修改后:

  1. 在Size子表录入动态列值,刷新页面会生成对应列的动态表格
  2. 在动态表格中输入数据,点击保存按钮
  3. 重新打开单据,数据会正确回填到表格中,说明已成功持久化到数据库

内容的提问来源于stack exchange,提问作者Aditya vig

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 12:04:59