PAYATHON 2026

NetSuite Workflow 创建 Customer Refund 时字段为空

支付小周

结论

不建议直接以 Sales Order 为来源创建 Customer Refund。Customer Refund 中的 customer、paymentmethod 和底部的 Apply Deposits 都受交易关系与字段联动影响,并不是普通文本字段。即使工作流暂时写入了值,NetSuite 也可能在记录初始化或字段 sourcing 时将其清空。

更可靠的处理流程是:

  1. 从 Sales Order 找到关联的 Customer Deposit。
  2. 使用 SuiteScript 将 Customer Deposit 转换为 Customer Refund。
  3. 转换完成后设置退款方式等字段。
  4. 检查 deposit 子列表,确认对应存款已勾选。

Memo 能保留 Customer 值,不代表同一个值也能写入 Customer 字段。Memo 是普通文本字段,而 Customer 是 List/Record 字段,会受到交易类型、主体、币种、子公司和字段 sourcing 的约束。

为什么 Customer 字段会被清空

Customer Refund 上的 Customer 会影响后续可用的币种、应收账款科目、存款列表、地址和退款方式。设置 Customer 时,NetSuite 会触发一系列 sourcing 和校验。

常见原因有:

  • 工作流写入了客户名称,而不是 Customer 的 internal ID。
  • Customer Refund 初始化后,其他字段的 sourcing 覆盖了 Customer。
  • Sales Order 与 Customer Refund 之间没有可直接转换的业务关系。
  • 在 OneWorld 环境中,客户、子公司、币种或账户组合不匹配。
  • 工作流动作执行得太早,此时 Customer Refund 尚未完成初始化。
  • 同一状态中的其他「设置字段值」动作触发了新的 sourcing,导致 Customer 被清空。
  • 工作流传递的是显示文本,而目标字段需要 List/Record 引用。

Memo 可以直接接受客户名称或显示文本,不会执行这些交易级校验,所以两个字段的表现并不相同。

工作流中可以先做的检查

如果仍想尝试纯工作流方案,可以先检查以下设置:

  1. Customer 字段使用「From Field」或等效的字段引用方式,来源是 Sales Order 的 Customer,而不是作为静态文本写入的客户名称。
  2. 确认传递的是客户 internal ID。
  3. 先设置 Customer,再设置依赖 Customer 的字段。
  4. 检查设置 Customer 后,是否又设置了 Subsidiary、Currency、Account、Custom Form 等可能触发 sourcing 的字段。
  5. 在目标记录完成初始化后再给 Customer 赋值,不要只在创建动作的最早阶段执行。
  6. 检查目标表单是否存在 Client Script、User Event Script 或其他 Workflow,并确认它们没有在保存前清空字段。

即使这些设置都正确,直接从 Sales Order 创建 Customer Refund,仍可能无法可靠处理 Apply Deposits。只要涉及存款应用,SuiteScript 通常更合适。

推荐方案:从 Customer Deposit 转换为 Customer Refund

如果当前账户支持 Customer Deposit -> Customer Refund 的 SuiteScript 转换,建议优先使用 record.transform()。这种方式由 NetSuite 建立 Customer、Currency 和可应用 Deposit 的上下文,比新建空白 Customer Refund 后逐个设置字段更可靠。

下面是 SuiteScript 2.x 的示意代码:

/**
 * @NApiVersion 2.1
 */
define(['N/record'], (record) => {
    function createRefundFromDeposit(customerDepositId, paymentMethodId) {
        const refund = record.transform({
            fromType: record.Type.CUSTOMER_DEPOSIT,
            fromId: customerDepositId,
            toType: record.Type.CUSTOMER_REFUND,
            isDynamic: true
        });

        if (paymentMethodId) {
            refund.setValue({
                fieldId: 'paymentmethod',
                value: paymentMethodId
            });
        }

        applyDeposit(refund, customerDepositId);

        return refund.save({
            enableSourcing: true,
            ignoreMandatoryFields: false
        });
    }

    function applyDeposit(refund, customerDepositId) {
        const lineCount = refund.getLineCount({
            sublistId: 'deposit'
        });

        for (let line = 0; line < lineCount; line += 1) {
            const depositId = refund.getSublistValue({
                sublistId: 'deposit',
                fieldId: 'doc',
                line
            });

            if (String(depositId) === String(customerDepositId)) {
                refund.selectLine({
                    sublistId: 'deposit',
                    line
                });

                refund.setCurrentSublistValue({
                    sublistId: 'deposit',
                    fieldId: 'apply',
                    value: true
                });

                refund.commitLine({
                    sublistId: 'deposit'
                });

                return;
            }
        }

        throw new Error(
            `Customer Deposit ${customerDepositId} is not available on the refund`
        );
    }

    return {
        createRefundFromDeposit
    };
});

deposit、doc 和 apply 是 Customer Refund 中常见的子列表和字段 ID,但最终应以当前账户版本的 Records Browser、SuiteScript Records Catalog 或实际脚本日志为准。自定义表单、功能启用状态和账户配置都可能影响可用字段。

如何取得 Customer Deposit 的 Payment Method

先加载 Customer Deposit,再读取其中的 paymentmethod:

const deposit = record.load({
    type: record.Type.CUSTOMER_DEPOSIT,
    id: customerDepositId,
    isDynamic: false
});

const customerId = deposit.getValue({
    fieldId: 'customer'
});

const paymentMethodId = deposit.getValue({
    fieldId: 'paymentmethod'
});

然后将 paymentMethodId 设置到 Customer Refund:

refund.setValue({
    fieldId: 'paymentmethod',
    value: paymentMethodId
});

字段设置顺序很重要。应先由转换过程正确建立 Customer,再设置 Payment Method,否则 Customer sourcing 可能再次清空 Payment Method。

收款时使用的 Payment Method 也不一定能直接用于退款。某些支付处理方式可能要求通过原支付交易、支付网关或指定退款流程处理。如果字段仍然为空,需要检查:

  • Payment Method 是否已启用。
  • 该方式是否适用于当前 Customer Refund。
  • Customer Deposit 与退款记录的币种是否一致。
  • 退款账户、子公司和支付处理配置是否匹配。
  • 是否有脚本或工作流在保存前覆盖 paymentmethod。

如何找到与 Sales Order 关联的 Customer Deposit

具体查找方式取决于系统如何保存这种关联。在标准 NetSuite 环境中,Sales Order 与 Customer Deposit 的关系可能记录在标准关联字段中,也可能通过自定义字段建立。

比较可靠的方式是在 Customer Deposit 上明确保存 Sales Order internal ID,例如使用以下自定义字段:

custbody_related_sales_order

然后通过 N/search 查找:

const depositSearch = search.create({
    type: search.Type.CUSTOMER_DEPOSIT,
    filters: [
        ['mainline', 'is', 'T'],
        'and',
        ['custbody_related_sales_order', 'anyof', salesOrderId]
    ],
    columns: [
        search.createColumn({
            name: 'internalid',
            sort: search.Sort.DESC
        })
    ]
});

不要只根据客户、金额或日期推断对应的 Customer Deposit。这些条件可能匹配到多笔存款,进而造成退款应用错误。

如果一张 Sales Order 可能关联多笔 Customer Deposit,还需要事先确定业务规则:全额应用所有存款、只应用指定存款,还是按照剩余可用金额依次应用。

Apply Deposits 是否必须使用脚本

工作流通常难以处理 Customer Refund 底部的 Apply Deposits 子列表。它不是普通的正文复选框,而是动态生成的交易应用子列表。

以下情况建议使用 SuiteScript:

  • 根据 internal ID 精确勾选某笔 Customer Deposit。
  • 控制本次应用金额。
  • 处理多笔 Deposit。
  • 校验 Deposit 是否仍有可退款余额。
  • 保存失败时返回明确错误。

可以保留现有工作流,并使用 Workflow Action Script 执行具体操作。工作流负责判断条件和触发动作,Workflow Action Script 则负责查找 Customer Deposit、转换 Customer Refund、设置 Payment Method 和应用 Deposit。

实施时的注意事项

  • 字段值应使用 internal ID,不要使用客户名称或交易编号。
  • 优先从 Customer Deposit 转换,不要根据 Sales Order 拼装空白退款记录。
  • 创建退款前检查 Deposit 是否还有可用余额,避免重复退款。
  • 校验 Customer Deposit 与 Sales Order 的 Customer、Subsidiary 和 Currency 是否一致。
  • 不要假设 Deposit 一定会出现在 deposit 子列表中。找不到时应终止保存,并记录具体原因。
  • 如果需要部分退款,除了勾选 apply,还要设置对应行的应用金额。
  • 在 Sandbox 中检查 Records Catalog,确认 customer、paymentmethod、deposit、doc、apply 等 internal ID。
  • 如果账户不支持上述转换路径,可以改为创建 Customer Refund,先设置 Customer,再加载并处理 deposit 子列表。具体哪些字段可以写入,仍应以当前账户的 Records Catalog 为准。

备注:内容仅供参考。