SuiteScript 2.0 部分核销 invoice 时出现错误
结论
这通常不是 Map/Reduce 对脚本类型的限制。record.transform() 可以在 MAP stage 中将 Invoice 转换为 Customer Payment,也支持部分核销。
问题更可能出在转换后的默认状态。Customer Payment 中,目标 Invoice 往往已经按全部未付金额勾选,apply 子列表中的核销金额也会填为全部未付金额。如果只修改付款总额,没有同步修改该行的核销金额,保存时就会因为付款金额与核销金额不一致而失败。
处理时需要确保:
payment字段等于本次实际收款金额,例如2.00;- 目标 Invoice 行的
apply为true; - 该行的
amount同样为2.00; - 取消勾选其他不需要核销的行;
- 子列表索引从
0开始; - 在动态模式下修改子列表后调用
commitLine()。
推荐处理方式
先将 Invoice 转换为 Customer Payment,明确设置付款金额,再遍历 apply 子列表,根据内部 ID 查找目标 Invoice。不要假定目标 Invoice 一定在第一行,也不要从索引 1 开始遍历。
动态模式示例
define(['N/record'], function (record) {
function createPartialPayment(invoiceId, paymentAmount) {
paymentAmount = Number(paymentAmount);
if (!invoiceId || !isFinite(paymentAmount) || paymentAmount <= 0) {
throw new Error('invoiceId 和 paymentAmount 必须有效,且付款金额必须大于 0');
}
var customerPayment = record.transform({
fromType: record.Type.INVOICE,
fromId: invoiceId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: true
});
var remaining = Number(
customerPayment.getValue({
fieldId: 'balance'
})
);
if (isFinite(remaining) && paymentAmount > remaining) {
throw new Error(
'付款金额不能大于 Invoice 的未付金额:' + remaining
);
}
customerPayment.setValue({
fieldId: 'payment',
value: paymentAmount
});
var lineCount = customerPayment.getLineCount({
sublistId: 'apply'
});
var invoiceFound = false;
for (var i = 0; i < lineCount; i++) {
customerPayment.selectLine({
sublistId: 'apply',
line: i
});
var appliedTransactionId = customerPayment.getCurrentSublistValue({
sublistId: 'apply',
fieldId: 'internalid'
});
var isTargetInvoice =
String(appliedTransactionId) === String(invoiceId);
customerPayment.setCurrentSublistValue({
sublistId: 'apply',
fieldId: 'apply',
value: isTargetInvoice
});
if (isTargetInvoice) {
customerPayment.setCurrentSublistValue({
sublistId: 'apply',
fieldId: 'amount',
value: paymentAmount
});
invoiceFound = true;
}
customerPayment.commitLine({
sublistId: 'apply'
});
}
if (!invoiceFound) {
throw new Error(
'Customer Payment 的 apply 子列表中未找到 Invoice:' +
invoiceId
);
}
return customerPayment.save({
enableSourcing: true,
ignoreMandatoryFields: false
});
}
return {
createPartialPayment: createPartialPayment
};
});
假设 Invoice 的未付金额为 26.66,调用时传入 2.00,最终应满足:
payment = 2.00
apply.amount = 2.00
apply.apply = true
标准模式更简单
如果不需要动态模式的字段联动,可以在标准模式下直接设置子列表值:
var customerPayment = record.transform({
fromType: record.Type.INVOICE,
fromId: invoiceId,
toType: record.Type.CUSTOMER_PAYMENT,
isDynamic: false
});
customerPayment.setValue({
fieldId: 'payment',
value: paymentAmount
});
var lineCount = customerPayment.getLineCount({
sublistId: 'apply'
});
var invoiceFound = false;
for (var i = 0; i < lineCount; i++) {
var appliedTransactionId = customerPayment.getSublistValue({
sublistId: 'apply',
fieldId: 'internalid',
line: i
});
var isTargetInvoice =
String(appliedTransactionId) === String(invoiceId);
customerPayment.setSublistValue({
sublistId: 'apply',
fieldId: 'apply',
line: i,
value: isTargetInvoice
});
if (isTargetInvoice) {
customerPayment.setSublistValue({
sublistId: 'apply',
fieldId: 'amount',
line: i,
value: paymentAmount
});
invoiceFound = true;
}
}
if (!invoiceFound) {
throw new Error('apply 子列表中未找到目标 Invoice');
}
var paymentId = customerPayment.save({
enableSourcing: true,
ignoreMandatoryFields: false
});
标准模式不需要调用 selectLine() 和 commitLine(),在 Map/Reduce 中通常更容易控制。
为什么 User Event 中可能正常
User Event 和 MAP stage 可能处于不同的记录状态或执行上下文。比如,User Event 中的页面操作可能已经触发了字段联动;Map/Reduce 直接操作记录时,则需要明确设置付款总额和核销行金额。
所以,User Event 成功而 Map/Reduce 失败,并不能证明 MAP stage 不支持 Customer Payment。排查时应对比两边是否以相同方式设置了以下内容:
payment;apply;apply.amount;- 子列表行提交;
- 账户、币种、期间和其他必填字段。
注意事项
子列表从 0 开始
SuiteScript 的子列表行号从 0 开始:
for (var i = 0; i < lineCount; i++) {
// ...
}
如果从 1 开始,就会跳过第一行,而转换生成的目标 Invoice 很可能正好在第一行。
不要依赖行位置
应通过 internalid 查找目标 Invoice:
String(appliedTransactionId) === String(invoiceId)
Customer Payment 的 apply 子列表可能包含该客户的其他未结交易,因此目标行的位置并不固定。
注意动态模式的字段联动
设置 payment 后,NetSuite 可能会重新计算 apply 子列表。建议按以下顺序操作:
- 执行
record.transform(); - 设置
payment; - 设置
apply和amount; - 在动态模式下提交子列表行;
- 保存记录。
防止并发重复付款
MAP stage 可以创建 Customer Payment,但如果输入数据中多次出现同一张 Invoice,不同的 MAP 调用可能会并发创建重复付款。应先按 Invoice 对输入去重,或通过外部唯一标识实现幂等控制。
如果需要将同一 Invoice 的多条输入汇总后再付款,可以把 Invoice ID 作为 key,在 REDUCE stage 中合并金额,然后只创建一次 Customer Payment。这是并发和数据组织问题,不是 Customer Payment API 对 MAP stage 的硬性限制。
保留完整错误信息
捕获异常时,应记录 name、message 和 stack:
try {
// 创建并保存 Customer Payment
} catch (e) {
log.error({
title: 'Partial payment failed',
details: {
name: e.name,
message: e.message,
stack: e.stack
}
});
throw e;
}
如果具体错误涉及会计期间、应收账户、币种、子公司或客户付款设置,还需要结合完整错误消息继续排查。不过,仅从全额付款成功、部分付款失败这一现象判断,应先检查 payment 与 apply.amount 是否保持一致。
备注:内容仅供参考。
