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;
}