NetSuite Workflow 创建 Customer Refund 时字段为空
结论
不建议直接以 Sales Order 为来源创建 Customer Refund。Customer Refund 中的 customer、paymentmethod 和底部的 Apply Deposits 都受交易关系与字段联动影响,并不是普通文本字段。即使工作流暂时写入了值,NetSuite 也可能在记录初始化或字段 sourcing 时将其清空。
更可靠的处理流程是:
- 从 Sales Order 找到关联的 Customer Deposit。
- 使用 SuiteScript 将 Customer Deposit 转换为 Customer Refund。
- 转换完成后设置退款方式等字段。
- 检查
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 可以直接接受客户名称或显示文本,不会执行这些交易级校验,所以两个字段的表现并不相同。
工作流中可以先做的检查
如果仍想尝试纯工作流方案,可以先检查以下设置:
- Customer 字段使用「From Field」或等效的字段引用方式,来源是 Sales Order 的 Customer,而不是作为静态文本写入的客户名称。
- 确认传递的是客户 internal ID。
- 先设置 Customer,再设置依赖 Customer 的字段。
- 检查设置 Customer 后,是否又设置了 Subsidiary、Currency、Account、Custom Form 等可能触发 sourcing 的字段。
- 在目标记录完成初始化后再给 Customer 赋值,不要只在创建动作的最早阶段执行。
- 检查目标表单是否存在 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 为准。
备注:内容仅供参考。
