PAYATHON 2026

SAP DI API:如何为多张发票添加一笔付款?

支付小周

结论

Payments.Add() 只能调用一次,并且要放在 foreach 循环之后。

在循环中,应逐行填写 _pay.Invoices 集合,并调用 _pay.Invoices.Add() 添加发票行。当前代码每次循环都会调用 _pay.Add(),SAP Business One 因此会把每张发票保存成一笔单独的付款。

原因

DI API 中的两个 Add() 用途不同:

  • _pay.Invoices.Add():在当前付款对象中添加一条发票明细。
  • _pay.Add():将整个付款对象提交到 SAP Business One。

调用顺序应为:

  1. 创建一个 Payments 对象。
  2. 设置付款抬头和付款方式。
  3. 循环添加所有发票行。
  4. 循环结束后,只调用一次 _pay.Add()。

代码示例

以下示例假设:

  • pay.CardCode 是付款对应的客户代码。
  • i.AmountApplied 是当前发票的核销金额,请替换为实际字段。
  • inv.DocEntry 是 SAP 发票的内部键。如果现有实体只有其他字段,需要先查询真正的 DocEntry。
  • 所有发票都属于同一个业务伙伴。
Payments payment = (Payments)company.GetBusinessObject(
    BoObjectTypes.oIncomingPayments);

payment.CardCode = pay.CardCode;
payment.DocDate = pay.DocDate;
payment.TaxDate = pay.TaxDate;
payment.DueDate = pay.DocDueDate;

// 建议设置 Series,让 SAP Business One 自动分配 DocNum。
payment.Series = 98;

payment.CashAccount = pay.CashAcct;
payment.CashSum = pay.CashSum;

int lineIndex = 0;

foreach (var detail in pay.Details)
{
    InvoiceHeader inv = context.InvoiceHeaders
        .FirstOrDefault(x => x.Id == detail.TransactionDocumentNo);

    if (inv == null)
    {
        throw new InvalidOperationException(
            $"未找到付款明细对应的发票:{detail.TransactionDocumentNo}");
    }

    if (lineIndex > 0)
    {
        payment.Invoices.Add();
    }

    payment.Invoices.SetCurrentLine(lineIndex);
    payment.Invoices.DocEntry = Convert.ToInt32(inv.DocEntry);
    payment.Invoices.InvoiceType = BoRcptInvTypes.it_Invoice;
    payment.Invoices.SumApplied = detail.AmountApplied;

    lineIndex++;
}

if (lineIndex == 0)
{
    throw new InvalidOperationException("付款至少需要关联一张发票。");
}

// 整笔付款只提交一次。
int errCode = payment.Add();

if (errCode != 0)
{
    company.GetLastError(out int sapErrorCode, out string sapErrorMessage);

    throw new InvalidOperationException(
        $"创建收款失败。SAP 错误 {sapErrorCode}:{sapErrorMessage}");
}

也可以先填写当前行,再调用 Invoices.Add(),但要注意不能在最后留下未填写的空白行。上面的写法只在添加第二张及后续发票前创建新行,处理起来更清楚。

每张发票应设置自己的核销金额

原代码在循环外执行:

_pay.Invoices.SumApplied = pay.TotalPaid;
_pay.Invoices.AppliedFC = pay.TotalPaid;

这样会把整笔付款金额写入第一条发票行。通常也不应同时填写本币和外币金额。应按每条明细分别赋值:

payment.Invoices.SumApplied = detail.AmountApplied;

如果付款和发票使用外币,而且当前场景确实需要填写外币核销金额,再使用:

payment.Invoices.AppliedFC = detail.AmountAppliedFC;

应根据公司本币、付款币种和发票币种,决定使用 SumApplied 还是 AppliedFC,不能无条件把同一金额同时写入两个字段。

注意事项

DocEntry 不能用 DocNum 代替

Payments.Invoices.DocEntry 需要的是发票内部键 DocEntry,不是用户界面中显示的单据编号 DocNum。

因此,下面的代码是否正确,要看 inv.DocumentNumber 实际保存的是什么:

payment.Invoices.DocEntry = Convert.ToInt32(inv.DocumentNumber);

如果 DocumentNumber 保存的是 OINV.DocNum,必须先查询对应的 OINV.DocEntry。例如:

SELECT DocEntry
FROM OINV
WHERE DocNum = 12345
  AND Series = 10

在使用多个编号系列的环境中,只按 DocNum 查询可能无法唯一确定发票。

不要直接读取 NNM1.NextNumber

不建议先查询 NNM1.NextNumber,再手动设置 DocNum:

select NextNumber from NNM1 where Series = 98

并发提交时,多个进程可能读到相同的号码。通常只需设置:

payment.Series = 98;

单据编号会由 SAP Business One 在执行 payment.Add() 时分配。

所有关联发票必须属于同一业务伙伴

一笔收款通常只能对应一个客户,因此所有发票的 CardCode 都应与下面的值一致:

payment.CardCode

如果明细中包含不同客户的发票,需要先按客户分组,再为每个客户分别创建一笔付款。

付款金额必须与付款方式匹配

所有发票行的核销金额合计,应与现金、转账、支票或信用卡等付款方式的金额保持合理关系。例如,只使用现金付款时,通常应满足:

CashSum = 所有发票行的 SumApplied 合计

如果涉及折扣、预付款、差额、外币汇率或多种付款方式,还需要根据实际业务设置相应字段。

需要修改的核心部分是:在循环中调用 payment.Invoices.Add() 管理发票行,循环结束后只调用一次 payment.Add(),提交整笔付款。

备注:内容仅供参考。