PAYATHON 2026

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 子列表。建议按以下顺序操作:

  1. 执行 record.transform();
  2. 设置 payment;
  3. 设置 apply 和 amount;
  4. 在动态模式下提交子列表行;
  5. 保存记录。

防止并发重复付款

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 是否保持一致。

备注:内容仅供参考。