Apply Customer Payments via API - Business Central

Hello,

I have a request from a client that wants to be able to apply customer ledger entries via an API. Has anyone had experience with this before? My idea was to create a Customer Ledger Entry Buffer table to send the records to via an API, but having some trouble figuring out how to get those posted to the Customer Ledger Entry table.

Any help would be appreciated. Thanks!

Hi @Brian_Schmidt,

You have to create a bound action in the API page (Creating and Interacting with an OData V4 Bound Action - Business Central | Microsoft Learn) to call after the buffer record is created. In this case, this buffer table can’t be temporary.

I would name this table as “API Application Log” (it’s more like a log than a buffer). You must see as Business Central applies customer ledger entries looking at the page “Apply Customer Entries”, in fact, the process is done by “Apply” procedure in “CustEntry-Apply Posted Entries” codeunit.

Best regards.

Thank you very much for the advice. I’ll give this a shot!

I have made a custom API page using this AL code:

page 8xxxxx "SF_API - ApplyInvoicePayment"
{
    PageType = API;
    APIPublisher = 'xxxxxx';
    APIGroup = 'xxxxxxx';
    APIVersion = 'v2.0';
    EntityName = 'sf_applyInvoicePayment';
    EntitySetName = 'sf_applyInvoicePayments';
    SourceTable = "Integer"; // dummy source table
    DelayedInsert = true;
    layout
    {
        area(content)
        {
            group(Fields)
            {
                field(InvoiceEntryId; InvoiceEntryId) { }
                field(PaymentEntryId; PaymentEntryId) { }
            }
        }
    }
    actions
    {
    }
    trigger OnInsertRecord(BelowxRec: Boolean): Boolean
    var
        InvoiceEntry: Record "Cust. Ledger Entry";
        PaymentEntry: Record "Cust. Ledger Entry";
        CustEntryApplyPosted: Codeunit "CustEntry-Apply Posted Entries";
        ApplyUnapplyParameters: Record "Apply Unapply Parameters" temporary;
        AppliesToID: Code[50];
    begin
        // Get and validate invoice entry
        if not InvoiceEntry.GetBySystemId(InvoiceEntryId) then
            Error('Invoice entry not found.');

        if InvoiceEntry."Document Type" <> InvoiceEntry."Document Type"::Invoice then
            Error('Entry is not a posted invoice.');

        if not PaymentEntry.GetBySystemId(PaymentEntryId) then
            Error('Payment entry not found: %1', PaymentEntryId);

        // Populate ApplyUnapplyParameters
        ApplyUnapplyParameters.Init();
        ApplyUnapplyParameters."Account Type" := ApplyUnapplyParameters."Account Type"::Customer;
        ApplyUnapplyParameters."Account No." := PaymentEntry."Customer No.";
        ApplyUnapplyParameters."Posting Date" := PaymentEntry."Posting Date";
        ApplyUnapplyParameters."Document No." := PaymentEntry."Document No.";
        ApplyUnapplyParameters."Entry No." := PaymentEntry."Entry No.";
        ApplyUnapplyParameters.Insert();

        // Apply this payment to the invoice
        CustEntryApplyPosted.Apply(InvoiceEntry, ApplyUnapplyParameters);
    end;

    var
        InvoiceEntryId: Guid;
        PaymentEntryId: Guid;
}

But my issue is that I get the error:

Cannot post because you did not specify which entry to apply. You must specify an entry in the Applies-to ID field for one or more open entries

What is wrong here? I thought .Apply() handles setting the Applies-to Id fields etc. Do I need to do something like this before calling .Apply()?

        AppliesToID := FORMAT(CREATEGUID);
        InvoiceEntry.LockTable();
        InvoiceEntry."Applies-to ID" := AppliesToID;
        InvoiceEntry.Modify();
        PaymentEntry.LockTable();
        PaymentEntry."Applies-to ID" := AppliesToID;
        PaymentEntry.Modify();

Even though I get the error, it does still appear to apply with an entry in the Detailed Cust. Ledg. Entry table and the Cust. Ledger Entry table record for the payment shows the Applying Entry field set to YES. But there are no Applied-To fields set at all. Is this correct behavior?

Here is my latest attempt. This is showing no error, and returns ‘created’ 201 status, but I don’t see that it actually does anything in BC.

page 83709 "SF_API - ApplyInvoicePayment"
{
    PageType = API;
    APIPublisher = 'XXXXXX';
    APIGroup = 'XXXXXX';
    APIVersion = 'v2.0';
    EntityName = 'sf_applyInvoicePayment';
    EntitySetName = 'sf_applyInvoicePayments';
    SourceTable = "Integer"; // dummy source table
    DelayedInsert = true;

    layout
    {
        area(content)
        {
            group(Fields)
            {
                field(InvoiceEntryId; InvoiceEntryId) { }
                field(PaymentEntryId; PaymentEntryId) { }
            }
        }
    }

    actions
    {
    }


    trigger OnInsertRecord(BelowxRec: Boolean): Boolean
    var
        InvoiceEntry: Record "Cust. Ledger Entry";
        PaymentEntry: Record "Cust. Ledger Entry";
        ApplyUnapplyParameters: Record "Apply Unapply Parameters" temporary;
        AppliesToID: Code[50];
        CustEntryApplyPosted: Codeunit "CustEntry-Apply Posted Entries";
        CustEntrySetApplID: Codeunit "Cust. Entry-SetAppl.ID";
    begin
        // Get and validate invoice entry
        if not InvoiceEntry.GetBySystemId(InvoiceEntryId) then
            Error('Invoice entry not found.');

        if InvoiceEntry."Document Type" <> InvoiceEntry."Document Type"::Invoice then
            Error('Entry is not a posted invoice.');

        if not PaymentEntry.GetBySystemId(PaymentEntryId) then
            Error('Payment entry not found: %1', PaymentEntryId);

        // Generate unique Applies-to ID for this application batch
        AppliesToID := FORMAT(CREATEGUID);

        // Assign Applies-to ID using the codeunit
        CustEntrySetApplID.SetApplId(InvoiceEntry, PaymentEntry, AppliesToID);

        // Populate ApplyUnapplyParameters
        ApplyUnapplyParameters.Init();
        ApplyUnapplyParameters."Account Type" := ApplyUnapplyParameters."Account Type"::Customer;
        ApplyUnapplyParameters."Account No." := PaymentEntry."Customer No.";
        ApplyUnapplyParameters."Posting Date" := PaymentEntry."Posting Date";
        ApplyUnapplyParameters."Document No." := PaymentEntry."Document No.";
        ApplyUnapplyParameters."Entry No." := PaymentEntry."Entry No.";
        ApplyUnapplyParameters.Insert();

        // Apply this payment to the invoice
        CustEntryApplyPosted.Apply(InvoiceEntry, ApplyUnapplyParameters);
    end;

    var
        InvoiceEntryId: Guid;
        PaymentEntryId: Guid;
}

Still would love any advice on this.

Hi Dustin,

You may want to try this:

page 70108 "ApplyCustCreditAPIGTK"
{
    APIGroup = 'GTKAPI';
    APIPublisher = 'GTKIPL';
    DelayedInsert = true;
    APIVersion = 'v2.0';
    EntityCaption = 'Apply Customer Credit Memmo API GTK';
    EntitySetCaption = 'Apply Customer Credit Memmo API GTKs';
    PageType = API;
    ODataKeyFields = SystemId;
    EntityName = 'applyCustCreditAPIGTK';
    EntitySetName = 'applyCustCreditAPIGTKs';
    SourceTable = "Integer";
    SourceTableTemporary = true;
    Extensible = false;
    layout
    {
        area(content)
        {
            repeater(Group)
            {
                field(Id; Rec.SystemId) { }
                field(number; Rec.Number) { }
                field(creditMemoId; CreditMemoEntryId)
                {
                    Caption = 'Credit Memo Id';
                }
                field(invoiceId; InvoiceEntryId)
                {
                    Caption = 'Invoice Id';
                }
            }
        }
    }

    actions
    {
    }

    trigger OnInsertRecord(BelowxRec: Boolean): Boolean
    var
        CreditMemoEntry: Record "Cust. Ledger Entry";
        InvoiceEntry: Record "Cust. Ledger Entry";
        InvoiceEntryNo: Integer;
        CreditMemoEntryNo: Integer;
        ApplyUnapplyParameters: Record "Apply Unapply Parameters" temporary;
        AppliesToID: Code[50];
        CustEntryApplyPosted: Codeunit "CustEntry-Apply Posted Entries";
        CustEntrySetApplID: Codeunit "Cust. Entry-SetAppl.ID";
    begin
        clear(CreditMemoEntryNo);
        Clear(InvoiceEntryNo);

        // Get and validate credit memo entry
        if not CreditMemoEntry.GetBySystemId(CreditMemoEntryId) then
            Error('Credit Memo entry not found: %1', CreditMemoEntryId)
        else
            CreditMemoEntryNo := CreditMemoEntry."Entry No.";

        if not CreditMemoEntry.get(CreditMemoEntryNo) then
            Error('Credit Memo entry not found: %1', CreditMemoEntryNo);

        if CreditMemoEntry."Document Type" <> CreditMemoEntry."Document Type"::"Credit Memo" then
            Error('Entry is not a posted credit memo.');

        if not CreditMemoEntry.Open then
            Error('Credit Memo status must be open.');

        // Get and validate invoice entry
        if not InvoiceEntry.GetBySystemId(InvoiceEntryId) then
            Error('Invoice entry not found: %1', InvoiceEntryId)
        else
            InvoiceEntryNo := InvoiceEntry."Entry No.";

        if not InvoiceEntry.get(InvoiceEntryNo) then
            Error('Invoice entry not found: %1', InvoiceEntryNo);

        if InvoiceEntry."Document Type" <> InvoiceEntry."Document Type"::"Invoice" then
            Error('Entry is not a posted invoice.');

        if not InvoiceEntry.Open then
            Error('Invoice status must be open.');

        if CreditMemoEntry."Customer No." <> InvoiceEntry."Customer No." then
            error('Credit Memo and Invoice must be belong to the customer.');

        // Generate unique Applies-to ID for this application batch
        AppliesToID := UserId + '-APIGTK';

        /*
        InvoiceEntry.setrange("Entry No.", InvoiceEntryNo);
        // Assign Applies-to ID using the codeunit
        CustEntrySetApplID.SetApplId(InvoiceEntry, CreditMemoEntry, AppliesToID);
        */
        InvoiceEntry.CalcFields("Remaining Amount");
        InvoiceEntry."Applies-to ID" := AppliesToID;
        InvoiceEntry."Applying Entry" := false;
        InvoiceEntry.VALIDATE("Amount to Apply", InvoiceEntry."Remaining Amount");
        InvoiceEntry.Modify();

        CreditMemoEntry.CalcFields("Remaining Amount");
        CreditMemoEntry."Applies-to ID" := AppliesToID;
        CreditMemoEntry."Applying Entry" := true;
        CreditMemoEntry.VALIDATE("Amount to Apply", CreditMemoEntry."Remaining Amount");
        CreditMemoEntry.Modify();

        // Populate ApplyUnapplyParameters
        ApplyUnapplyParameters.Init();
        ApplyUnapplyParameters."Account Type" := ApplyUnapplyParameters."Account Type"::Customer;
        ApplyUnapplyParameters."Account No." := CreditMemoEntry."Customer No.";
        ApplyUnapplyParameters."Posting Date" := CreditMemoEntry."Posting Date";
        ApplyUnapplyParameters."Document No." := CreditMemoEntry."Document No.";
        ApplyUnapplyParameters."Entry No." := CreditMemoEntry."Entry No.";
        ApplyUnapplyParameters.Insert();

        // Apply this credit memo to invoice
        CustEntryApplyPosted.Apply(CreditMemoEntry, ApplyUnapplyParameters);

    end;

    var
        CreditMemoEntryId: Guid;
        InvoiceEntryId: Guid;

}