PAYATHON 2026

SuiteScript 将 Invoice 转为 Customer Payment 时报错

支付老李

结论

Invoice 可以 transform 为 Customer Payment。这次失败并非因为不支持 transform,而是生成的 Customer Payment 缺少必填字段 Project 和 Department,因此无法保存。

在 record.transform() 返回付款记录后、调用 save() 前,使用 setValue() 填写这两个字段:

var paymentRecord = record.transform({
    fromType: record.Type.INVOICE,
    fromId: invoiceId,
    toType: record.Type.CUSTOMER_PAYMENT,
    isDynamic: true
});

paymentRecord.setValue({
    fieldId: "department",
    value: departmentId
});

paymentRecord.setValue({
    fieldId: "custbody_project", // 替换为账户中 Project 字段的真实 ID
    value: projectId
});

var paymentId = paymentRecord.save();

错误信息显示的是字段标签,不一定是 SuiteScript 使用的字段 ID。Department 的标准字段 ID 通常是 department。Project 则很可能是自定义正文域、自定义 segment,或者被工作流或脚本设为必填,因此需要先确认它的真实字段 ID。

为什么会在 save() 时报错

record.transform() 只会创建一条尚未保存的 Customer Payment 记录。NetSuite 通常会将客户、币种、应收账款科目和来源 Invoice 等信息带入付款记录,但账户中的自定义字段未必会随之继承。

执行下面这行代码时:

var rid3 = objRecord3.save();

NetSuite 才会进行完整的必填字段验证,检查内容包括:

  • Customer Payment 表单上的必填设置;
  • Accounting Preferences 中的部门分类要求;
  • 自定义字段的必填设置;
  • Workflow 设置的必填规则;
  • Client Script、User Event 等脚本中的验证逻辑。

所以,这些字段需要在 transform 完成后、save 之前填写。

从 Invoice 复制字段

如果 Invoice 上已经有 Department 和 Project,可以先读取 Invoice,再将字段值复制到 Customer Payment。

define(["N/record"], function(record) {
    function afterSubmit(scriptContext) {
        var salesOrderId = scriptContext.newRecord.id;

        var fulfillmentRecord = record.transform({
            fromType: record.Type.SALES_ORDER,
            fromId: salesOrderId,
            toType: record.Type.ITEM_FULFILLMENT,
            isDynamic: true
        });

        fulfillmentRecord.save();

        var invoiceRecord = record.transform({
            fromType: record.Type.SALES_ORDER,
            fromId: salesOrderId,
            toType: record.Type.INVOICE,
            isDynamic: true
        });

        var invoiceId = invoiceRecord.save();

        var sourceInvoice = record.load({
            type: record.Type.INVOICE,
            id: invoiceId,
            isDynamic: false
        });

        var departmentId = sourceInvoice.getValue({
            fieldId: "department"
        });

        // 示例字段 ID,必须替换为账户中 Project 字段的真实 ID
        var projectId = sourceInvoice.getValue({
            fieldId: "custbody_project"
        });

        var paymentRecord = record.transform({
            fromType: record.Type.INVOICE,
            fromId: invoiceId,
            toType: record.Type.CUSTOMER_PAYMENT,
            isDynamic: true
        });

        if (departmentId) {
            paymentRecord.setValue({
                fieldId: "department",
                value: departmentId
            });
        }

        if (projectId) {
            paymentRecord.setValue({
                fieldId: "custbody_project",
                value: projectId
            });
        }

        var paymentId = paymentRecord.save();

        log.audit({
            title: "Customer Payment created",
            details: {
                invoiceId: invoiceId,
                paymentId: paymentId
            }
        });
    }

    return {
        afterSubmit: afterSubmit
    };
});

如果这两个值来自 Sales Order,也可以直接从 scriptContext.newRecord 读取,无须重新加载 Invoice:

var departmentId = scriptContext.newRecord.getValue({
    fieldId: "department"
});

var projectId = scriptContext.newRecord.getValue({
    fieldId: "custbody_project"
});

这样做的前提是 Sales Order 上确实存在这些字段,而且字段值符合 Customer Payment 的取值范围。

如何确认 Project 的字段 ID

不要把错误信息中的 Project 直接当成字段 ID:

// 通常是错误的
paymentRecord.setValue({
    fieldId: "Project",
    value: projectId
});

应通过 Customer Payment 表单或自定义字段设置确认:

  1. 打开一条 Customer Payment。
  2. 启用 NetSuite 的“显示内部 ID”偏好。
  3. 查看 Project 字段的内部 ID。
  4. 如果它是自定义正文域,ID 通常类似 custbody_project。
  5. 如果它是自定义 segment,ID 可能类似 cseg_project,具体取决于账户配置。
  6. 如果页面上找不到该字段,检查 Workflow、User Event 或当前使用的 Custom Form,确认是否有规则在保存时强制要求填写该字段。

setValue() 的 value 应使用字段值的内部 ID,而不是界面上显示的名称。例如:

paymentRecord.setValue({
    fieldId: "department",
    value: 12
});

这里的 12 是某个 Department 的内部 ID,而不是字符串 "Sales Department"。

必要时显式应用 Invoice

正常情况下,由 Invoice transform 得到的 Customer Payment 会自动关联来源发票。如果账户配置或脚本导致 apply 状态不正确,可以在保存前检查对应行,并显式勾选:

var lineCount = paymentRecord.getLineCount({
    sublistId: "apply"
});

for (var i = 0; i < lineCount; i++) {
    paymentRecord.selectLine({
        sublistId: "apply",
        line: i
    });

    var appliedInvoiceId = paymentRecord.getCurrentSublistValue({
        sublistId: "apply",
        fieldId: "internalid"
    });

    if (String(appliedInvoiceId) === String(invoiceId)) {
        paymentRecord.setCurrentSublistValue({
            sublistId: "apply",
            fieldId: "apply",
            value: true
        });

        paymentRecord.commitLine({
            sublistId: "apply"
        });

        break;
    }
}

只有 transform 后没有自动应用发票时,才需要加入这段逻辑。

注意事项

不建议用以下方式绕过错误:

paymentRecord.save({
    ignoreMandatoryFields: true
});

这个选项可能会绕过一部分自定义必填检查,但不保证能跳过系统必填字段、工作流或其他脚本的校验,还可能生成分类信息不完整的会计交易。更稳妥的处理方式是确认 Project 和 Department 的真实字段 ID,并在保存付款记录前为它们设置有效的内部 ID。

如果已经通过 setValue() 设置字段,但仍然出现相同错误,需要继续检查 Customer Payment 使用的 Custom Form、Workflow,以及部署在 Customer Payment 上的 User Event。错误中显示的字段名称可能来自这些自定义验证规则。

备注:内容仅供参考。