Codeunit 102 Import Consolidation from API
- App
- Base Application
- Namespace
- Microsoft.Finance.Consolidation
- Versions
- 23-28
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/Finance/Consolidation/ImportConsolidationFromAPI.Codeunit.al992 lines, Copyright (c) Microsoft Corporation. MIT
// ------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
// ------------------------------------------------------------------------------------------------
namespace Microsoft.Finance.Consolidation;
using Microsoft.Finance.Currency;
using Microsoft.Finance.Dimension;
using Microsoft.Finance.GeneralLedger.Account;
using Microsoft.Finance.GeneralLedger.Journal;
using Microsoft.Finance.GeneralLedger.Ledger;
using Microsoft.Finance.GeneralLedger.Setup;
using Microsoft.Foundation.Period;
using System.Azure.KeyVault;
using System.Environment;
using System.Reflection;
using System.Security.Authentication;
using System.Telemetry;
using System.Utilities;
/// <summary>
/// Implements consolidation data import from external Business Central instances via API endpoints.
/// Handles authentication, data retrieval, and processing of subsidiary company data through REST API calls.
/// </summary>
/// <remarks>
/// Modern API-based consolidation import supporting OAuth authentication and secure data exchange.
/// Integrates with Microsoft Entra ID for authentication and supports telemetry and request logging.
/// Alternative to database-based consolidation for cloud and hybrid scenarios.
/// Extensibility: Interface implementation allows for custom API import providers.
/// </remarks>
codeunit 102 "Import Consolidation from API" implements "Import Consolidation Data"
{
Permissions = tabledata "General Ledger Setup" = R,
tabledata "Gen. Journal Batch" = R;
var
ConsolidationSetup: Record "Consolidation Setup";
BusinessUnitBeingImported: Record "Business Unit";
AADTenantId: Text;
BusinessUnitAPIBaseUrl: Text;
LogRequests: Boolean;
UrlNotBCMsg: Label 'The URL provided is not in the businesscentral.dynamics.com domain. Do you want to continue?';
BusinessUnitNotConfiguredForAPIErr: Label 'Business unit %1 is not configured for API import. You can configure it in the "Business Unit" card page.', Comment = '%1 - Business unit code';
EntriesAtClosingDateErr: Label 'Entries posted in a closing date %1 were found while consolidating G/L account %2', Comment = '%1 - Closing date, %2 - G/L account no.';
APIStatusCodeErr: Label 'API call to subsidiary BC company failed with status code %1 - %2', Comment = '%1 - Status code, %2 - Status text';
APITooManyRequestsErr: Label 'Too many requests to subsidiary BC company''s API. Attempt the consolidation when no other processes are using the APIs or configure the thresholds in the "Consolidation Setup" page.';
APIGetSingleGotOtherErr: Label 'API call expected 1 result, got %1', Comment = '%1 - Number of results';
NeedSelectCompanyErr: Label 'You need to select a company to extract data from.';
SelectOnlyOneCompanyErr: Label 'You can only select one company per business unit.';
PrivacyConsentNoGUIErr: Label 'Getting privacy consent requires user interaction.';
BusinessUnitConsentErr: Label 'The business unit %1 needs to consent for their data to be transferred to the consolidation company. Open the page Consolidation Setup in that company and enable the company as subsidiary.', Comment = '%1 - Business unit code';
FinancialConsolidationServiceNameTxt: Label 'Financial Consolidation', Locked = true;
SecurityAuditConsentMissingTxt: Label 'Cross-tenant consolidation data access was denied: subsidiary business unit %1 has not enabled itself as a subsidiary.', Locked = true, Comment = '%1 - Business Unit Code';
ExchangeRateNotDefinedInBusinessUnitErr: Label 'Exchange rate for %1 is not defined in Business Unit.', Comment = '%1 - Currency Code';
PostingDateFilterTok: Label 'postingDate ge %1 and postingDate le %2', Locked = true, Comment = '%1 - Starting date, %2 - Ending date';
PostingClosingDateFilterTok: Label 'postingDate gt %1 and postingDate lt %2', Locked = true, Comment = '%1 - Starting date, %2 - Ending date';
CurrencyCodeFilterTok: Label 'currencyCode eq ''%1''', Locked = true, Comment = '%1 - Currency code';
DimensionCodeFilterTok: Label 'code eq ''%1''', Locked = true, Comment = '%1 - Dimension code';
DimensionConsolidationCodeFilterTok: Label 'consolidationCode eq ''%1''', Locked = true, Comment = '%1 - Dimension code';
GLAccountFilterTok: Label 'accountNumber eq ''%1''', Locked = true, Comment = '%1 - G/L account no.';
ClientIdAKVKeyTok: Label 'bctobc-finconsolid-clientid', Locked = true;
ClientCertificateAKVKeyTok: Label 'bctobc-finconsolid-clientcertname', Locked = true;
ClientSecretAKVKeyTok: Label 'bctobc-finconsolid-clientsecret', Locked = true;
AuthorityURLTok: Label 'https://login.microsoftonline.com/%1/oauth2/v2.0/authorize', Locked = true;
PPEAuthorityURLTok: Label 'https://login.windows-ppe.net/%1/oauth2', Locked = true;
FinancialsScopeTok: Label 'https://api.businesscentral.dynamics.com/Financials.ReadWrite.All', Locked = true;
PPEFinancialsScopeTok: Label 'https://api.businesscentral.dynamics-tie.com/.default', Locked = true;
RedirectURLTok: Label 'https://businesscentral.dynamics.com/OAuthLanding.htm', Locked = true;
PPERedirectUrlTok: Label 'https://businesscentral.dynamics-tie.com/OAuthLanding.htm', Locked = true;
GetTokenCalledFromOnPremMsg: Label 'Get token called from OnPrem', Locked = true;
RefreshingTokenMsg: Label 'Refreshing token for Financial Consolidations', Locked = true;
AKVSecretNotFoundMsg: Label 'AKV key not found', Locked = true;
FeatureTelemetryNameTok: Label 'Financial Consolidations - API', Locked = true;
ApiVersionTok: Label 'v2.0', Locked = true;
CompaniesEndpointSegmentTok: Label '/companies', Locked = true;
CurrencyExchangeRatesSegmentTok: Label '/currencyExchangeRates', Locked = true;
LastCurrencyExchangeRateQueryOptionsTok: Label '$filter=%1 and startingDate le %2&$orderby=startingDate desc&$top=1&$count=true', Locked = true, Comment = '%1 - Currency code filter, %2 - Date';
internal procedure GetPrivacyConsentChoice(): Boolean
var
PrivacyStatement: Page "Privacy Statement";
begin
if not GuiAllowed() then
Error(PrivacyConsentNoGUIErr);
if PrivacyStatement.RunModal() <> Action::OK then
exit(false);
exit(PrivacyStatement.GetConsentState());
end;
internal procedure GetFeatureTelemetryName(): Text
begin
exit(FeatureTelemetryNameTok);
end;
internal procedure AcquireTokenAndStoreInIsolatedStorage(BusinessUnit: Record "Business Unit")
var
StorageKey: Text;
Token: SecretText;
begin
SetAPIParameters(BusinessUnit."AAD Tenant ID", BusinessUnit."Log Requests");
StorageKey := IsolatedStorageKey(AADTenantId);
Token := GetToken(CurrentAuthorityUrl());
if (not EncryptionEnabled()) or (GetTokenLength(Token) > 215) then
IsolatedStorage.Set(StorageKey, Token, DataScope::Company)
else
IsolatedStorage.SetEncrypted(StorageKey, Token, DataScope::Company);
end;
[NonDebuggable]
local procedure GetTokenLength(Token: SecretText): Integer
begin
exit(StrLen(Token.Unwrap()));
end;
internal procedure IsStoredTokenValidForBusinessUnit(BusinessUnit: Record "Business Unit"): Boolean
var
StatusCode: Integer;
Token: SecretText;
StatusReason, StorageKey : Text;
begin
StorageKey := IsolatedStorageKey(GuidToText(BusinessUnit."AAD Tenant ID"));
if not IsolatedStorage.Contains(StorageKey, DataScope::Company) then
exit(false);
IsolatedStorage.Get(StorageKey, DataScope::Company, Token);
HttpGetText(TestEndpointForBusinessUnit(BusinessUnit), Token, StatusCode, StatusReason);
if StatusCode <> 200 then
exit(false);
exit(true);
end;
local procedure CurrentAuthorityUrl(): Text
begin
if IsPPE() then
exit(StrSubstNo(PPEAuthorityURLTok, AADTenantId));
exit(StrSubstNo(AuthorityURLTok, AADTenantId));
end;
local procedure IsPPE(): Boolean
begin
exit(StrPos(LowerCase(GetUrl(ClientType::Web)), 'businesscentral.dynamics-tie.com') <> 0);
end;
internal procedure GetCurrencyFactorFromBusinessUnit(var BusinessUnit: Record "Business Unit"): Decimal
var
GeneralLedgerSetup: Record "General Ledger Setup";
QueryOptions, Response : Text;
ExchangeRateAmount, Result : Decimal;
ExchangeRatesCount: Integer;
JsonObject: JsonObject;
JsonToken: JsonToken;
begin
SetAPIParameters(BusinessUnit."AAD Tenant ID", BusinessUnit."Log Requests");
if not IsStoredTokenValidForBusinessUnit(BusinessUnit) then
AcquireTokenAndStoreInIsolatedStorage(BusinessUnit);
GeneralLedgerSetup.Get();
QueryOptions := StrSubstNo(LastCurrencyExchangeRateQueryOptionsTok, StrSubstNo(CurrencyCodeFilterTok, GeneralLedgerSetup."LCY Code"), FormatDateForAPICall(WorkDate()));
Response := HttpGetTextWithStatusHandling(APIV2RootEndpointForBusinessUnit(BusinessUnit) + CurrencyExchangeRatesSegmentTok + '?' + QueryOptions);
ExchangeRatesCount := ParseCount(Response);
if ExchangeRatesCount = 0 then
Error(ExchangeRateNotDefinedInBusinessUnitErr, GeneralLedgerSetup."LCY Code");
JsonObject := ParseDataSingle(Response);
JsonObject.Get('exchangeRateAmount', JsonToken);
ExchangeRateAmount := JsonToken.AsValue().AsDecimal();
if ExchangeRateAmount = 0 then
exit(0);
JsonObject.Get('relationalExchangeRateAmount', JsonToken);
Result := JsonToken.AsValue().AsDecimal() / ExchangeRateAmount;
exit(Result);
end;
internal procedure SelectCompanyForBusinessUnit(var BusinessUnit: Record "Business Unit")
var
TempCompany: Record Company temporary;
FeatureTelemetry: Codeunit "Feature Telemetry";
CompanySelector: Page "Company Selector";
CompanyJsonToken: JsonToken;
PropertyJsonToken: JsonToken;
begin
AcquireTokenAndStoreInIsolatedStorage(BusinessUnit);
foreach CompanyJsonToken in HttpGet(CompaniesEndpointForBusinessUnit(BusinessUnit)) do begin
CompanyJsonToken.AsObject().Get('id', PropertyJsonToken);
TempCompany.Id := PropertyJsonToken.AsValue().AsText();
CompanyJsonToken.AsObject().Get('name', PropertyJsonToken);
TempCompany.Name := CopyStr(PropertyJsonToken.AsValue().AsText(), 1, MaxStrLen(TempCompany.Name));
CompanyJsonToken.AsObject().Get('displayName', PropertyJsonToken);
TempCompany."Display Name" := CopyStr(PropertyJsonToken.AsValue().AsText(), 1, MaxStrLen(TempCompany."Display Name"));
TempCompany.Insert();
end;
CompanySelector.SetCompanies(TempCompany);
CompanySelector.LookupMode(true);
Commit();
if CompanySelector.RunModal() <> Action::LookupOK then
Error(NeedSelectCompanyErr);
CompanySelector.GetSelectedCompany(TempCompany);
if TempCompany.Count() > 1 then
Error(SelectOnlyOneCompanyErr);
TempCompany.FindFirst();
BusinessUnit."External Company Id" := TempCompany.Id;
BusinessUnit.Validate("External Company Name", TempCompany.Name);
SetBusinessUnitAPIBaseUrl(BusinessUnit);
if not VerifySubsidiaryConsent() then begin
Session.LogSecurityAudit(
FinancialConsolidationServiceNameTxt, SecurityOperationResult::Failure,
StrSubstNo(SecurityAuditConsentMissingTxt, BusinessUnit.Code),
AuditCategory::CustomerFacing);
Error(BusinessUnitConsentErr, BusinessUnit.Code);
end;
FeatureTelemetry.LogUptake('0000KOL', GetFeatureTelemetryName(), Enum::"Feature Uptake Status"::"Set up");
Session.LogMessage('0000KTR', 'Configured new external BC company:' + UserSecurityId() + ', ' + BusinessUnit.Code, Verbosity::Normal, DataClassification::EndUserPseudonymousIdentifiers, TelemetryScope::All, 'Category', ConsolidationSetup.GetTelemetryCategory());
end;
local procedure SetBusinessUnitAPIBaseUrl(var BusinessUnit: Record "Business Unit")
begin
BusinessUnitAPIBaseUrl := CompaniesEndpointForBusinessUnit(BusinessUnit) + '(' + GuidToText(BusinessUnit."External Company Id") + ')';
end;
internal procedure GetAADTenantIdFromBCUrl(Url: Text): Text
var
Matches: Record Matches;
Regex: Codeunit Regex;
begin
Regex.Match(Url, '[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}', 0, Matches);
if Matches.FindFirst() then
exit(Matches.ReadValue());
end;
internal procedure ValidateBCUrl(Url: Text): Boolean
var
UnexpectedDomain: Boolean;
begin
if not Url.StartsWith('https://') then
exit(false);
if GetAADTenantIdFromBCUrl(Url) = '' then
exit(false);
if IsPPE() then
UnexpectedDomain := StrPos(LowerCase(Url), 'businesscentral.dynamics-tie.com') = 0
else
UnexpectedDomain := StrPos(LowerCase(Url), 'businesscentral.dynamics.com') = 0;
if not UnexpectedDomain then
exit(true);
exit(Confirm(UrlNotBCMsg, false));
end;
local procedure APIV2RootEndpointForBusinessUnit(BusinessUnit: Record "Business Unit"): Text
begin
exit(BusinessUnit."BC API URL" + ApiVersionTok);
end;
local procedure CompaniesEndpointForBusinessUnit(BusinessUnit: Record "Business Unit"): Text
begin
exit(APIV2RootEndpointForBusinessUnit(BusinessUnit) + CompaniesEndpointSegmentTok);
end;
local procedure TestEndpointForBusinessUnit(BusinessUnit: Record "Business Unit"): Text
begin
exit(CompaniesEndpointForBusinessUnit(BusinessUnit) + '?$top=0');
end;
local procedure GuidToText(Guid: Guid): Text
begin
exit(LowerCase(Guid).Replace('{', '').Replace('}', ''));
end;
local procedure IsolatedStorageKey(AADTenantIDOfBusinessUnit: Text): Text
begin
exit('fc-' + AADTenantIDOfBusinessUnit);
end;
local procedure SetAPIParameters(NewAADTenantID: Guid; NewLogRequests: Boolean)
begin
AADTenantId := GuidToText(NewAADTenantID);
LogRequests := NewLogRequests;
end;
local procedure GetToken(AuthorityURL: Text): SecretText
var
AzureKeyVault: Codeunit "Azure Key Vault";
EnvironmentInformation: Codeunit "Environment Information";
OAuth2: Codeunit OAuth2;
Scopes: List of [Text];
ClientSecret: SecretText;
[NonDebuggable]
ClientId: Text;
CertificateName, FinancialsScope, RedirectURL : Text;
AccessToken, Certificate : SecretText;
IdToken, AuthError : Text;
IsEnvPPE: Boolean;
begin
IsEnvPPE := IsPPE();
Session.LogMessage('0000KTS', RefreshingTokenMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
if not EnvironmentInformation.IsSaaS() then begin
Session.LogMessage('0000KOB', GetTokenCalledFromOnPremMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
exit(AccessToken);
end;
if not AzureKeyVault.GetAzureKeyVaultSecret(ClientIdAKVKeyTok, ClientId) then begin
Session.LogMessage('0000KOE', AKVSecretNotFoundMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
exit(AccessToken);
end;
FinancialsScope := FinancialsScopeTok;
RedirectURL := RedirectURLTok;
if IsEnvPPE then begin
FinancialsScope := PPEFinancialsScopeTok;
RedirectURL := PPERedirectUrlTok;
if not AzureKeyVault.GetAzureKeyVaultSecret(ClientSecretAKVKeyTok, ClientSecret) then begin
Session.LogMessage('0000KOI', AKVSecretNotFoundMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
exit(AccessToken);
end
end else begin
if not AzureKeyVault.GetAzureKeyVaultSecret(ClientCertificateAKVKeyTok, CertificateName) then begin
Session.LogMessage('0000KOC', AKVSecretNotFoundMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
exit(AccessToken);
end;
if not AzureKeyVault.GetAzureKeyVaultCertificate(CertificateName, Certificate) then begin
Session.LogMessage('0000KOD', AKVSecretNotFoundMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', ConsolidationSetup.GetTelemetryCategory());
exit(AccessToken);
end;
end;
Scopes.Add(FinancialsScope);
if IsEnvPPE then begin
if OAuth2.AcquireAuthorizationCodeTokenFromCache(ClientId, ClientSecret, RedirectURL, AuthorityURL, Scopes, AccessToken) then
if not AccessToken.IsEmpty() then
exit(AccessToken);
if OAuth2.AcquireTokensByAuthorizationCode(ClientId, ClientSecret, AuthorityURL, RedirectURL, Scopes, Enum::"Prompt Interaction"::"Admin Consent", AccessToken, IdToken, AuthError) then
exit(AccessToken);
end else begin
if OAuth2.AcquireAuthorizationCodeTokenFromCacheWithCertificate(ClientId, Certificate, RedirectURL, AuthorityURL, Scopes, AccessToken) then
if not AccessToken.IsEmpty() then
exit(AccessToken);
if OAuth2.AcquireTokensByAuthorizationCodeWithCertificate(ClientId, Certificate, AuthorityURL, RedirectUrl, Scopes, Enum::"Prompt Interaction"::"Select Account", AccessToken, IdToken, AuthError) then
exit(AccessToken);
end;
end;
local procedure HttpGetTextWithTokenInIsolatedStorage(Uri: Text; var StatusCode: Integer; var StatusReasonPhrase: Text): Text
var
Response: Text;
StorageKey: Text;
Token: SecretText;
begin
StorageKey := IsolatedStorageKey(AADTenantId);
if IsolatedStorage.Contains(StorageKey, DataScope::Company) then
IsolatedStorage.Get(StorageKey, DataScope::Company, Token);
Response := HttpGetText(Uri, Token, StatusCode, StatusReasonPhrase);
if StatusCode = 401 then begin
Session.LogMessage('0000OUO', 'Token expired, refreshing', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', ConsolidationSetup.GetTelemetryCategory());
AcquireTokenAndStoreInIsolatedStorage(BusinessUnitBeingImported);
IsolatedStorage.Get(StorageKey, DataScope::Company, Token);
Response := HttpGetText(Uri, Token, StatusCode, StatusReasonPhrase);
if StatusCode <> 200 then
Session.LogMessage('0000OUP', 'Request failed after refreshing token', Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', ConsolidationSetup.GetTelemetryCategory());
end;
exit(Response);
end;
local procedure HttpGetText(Uri: Text; Token: SecretText; var StatusCode: Integer; var StatusReasonPhrase: Text): Text
var
HttpRequestMessage: HttpRequestMessage;
HttpHeaders: HttpHeaders;
HttpResponseMessage: HttpResponseMessage;
HttpClient: HttpClient;
Response: Text;
begin
HttpRequestMessage.Method('GET');
HttpRequestMessage.SetRequestUri(Uri);
HttpHeaders := HttpClient.DefaultRequestHeaders();
HttpHeaders.Add('Accept', 'application/json');
HttpHeaders.Add('Authorization', SecretStrSubstNo('Bearer %1', Token));
HttpClient.Send(HttpRequestMessage, HttpResponseMessage);
StatusCode := HttpResponseMessage.HttpStatusCode();
StatusReasonPhrase := HttpResponseMessage.ReasonPhrase();
HttpResponseMessage.Content().ReadAs(Response);
LogRequestIfLoggingEnabled(Uri, Response, StatusCode);
exit(Response);
end;
local procedure LogRequestIfLoggingEnabled(Uri: Text; Response: Text; StatusCode: Integer)
var
ConsolidationLogEntry: Record "Consolidation Log Entry";
OutStream: OutStream;
begin
if not LogRequests then
exit;
ConsolidationLogEntry."Status Code" := StatusCode;
ConsolidationLogEntry."Request URI Preview" := CopyStr(Uri, 1, MaxStrLen(ConsolidationLogEntry."Request URI Preview"));
ConsolidationLogEntry.Insert();
ConsolidationLogEntry."Request URI".CreateOutStream(OutStream, TextEncoding::UTF8);
OutStream.WriteText(Uri);
ConsolidationLogEntry.Modify();
Clear(OutStream);
ConsolidationLogEntry.Response.CreateOutStream(OutStream, TextEncoding::UTF8);
OutStream.WriteText(Response);
ConsolidationLogEntry.Modify();
Commit();
end;
local procedure HttpGetTextWithStatusHandling(Uri: Text): Text
var
StatusReasonPhrase, Response : Text;
StatusCode, Attempt : Integer;
begin
while Attempt <= ConsolidationSetup.MaxAttempts429 do begin
Response := HttpGetTextWithTokenInIsolatedStorage(Uri, StatusCode, StatusReasonPhrase);
case StatusCode of
200:
exit(Response);
429:
System.Sleep(ConsolidationSetup.WaitMsRetries);
else
Error(APIStatusCodeErr, StatusCode, StatusReasonPhrase);
end;
Attempt += 1;
end;
Error(APITooManyRequestsErr);
end;
/// <summary>
/// Imports consolidation data for a specific business unit from external Business Central API.
/// Handles API connection setup, data retrieval, and consolidation processing initialization.
/// </summary>
/// <param name="ConsolidationProcess">Consolidation Process record defining import parameters</param>
/// <param name="BusinessUnit">Business Unit configured for API data import</param>
/// <param name="BusUnitConsolidationData">Business Unit Consolidation Data record for processing context</param>
procedure ImportConsolidationDataForBusinessUnit(ConsolidationProcess: Record "Consolidation Process"; BusinessUnit: Record "Business Unit"; var BusUnitConsolidationData: Record "Bus. Unit Consolidation Data")
var
GeneralLedgerSetup: Record "General Ledger Setup";
GenJournalBatch: Record "Gen. Journal Batch";
SelectedDimension: Record "Selected Dimension";
Consolidate: Codeunit Consolidate;
GLSetupJsonObject: JsonObject;
ValidateNoPostingsAtClosingDates: Boolean;
AdditionalReportingCurrencyCode: Code[10];
CurrencyCode: Code[10];
begin
GeneralLedgerSetup.Get();
ConsolidationSetup.GetOrCreateWithDefaults();
BusUnitConsolidationData.GetConsolidate(Consolidate);
Clear(Consolidate);
if BusinessUnit."Default Data Import Method" <> BusinessUnit."Default Data Import Method"::API then
Error(BusinessUnitNotConfiguredForAPIErr, BusinessUnit.Code);
if (BusinessUnit."BC API URL" = '') or IsNullGuid(BusinessUnit."External Company Id") then
Error(BusinessUnitNotConfiguredForAPIErr, BusinessUnit.Code);
Session.LogMessage('0000OUQ', 'Starting data import for business unit', Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', ConsolidationSetup.GetTelemetryCategory());
SetAPIParameters(BusinessUnit."AAD Tenant ID", BusinessUnit."Log Requests");
SetBusinessUnitAPIBaseUrl(BusinessUnit);
BusinessUnitBeingImported.Get(BusinessUnit.Code);
if GeneralLedgerSetup."Journal Templ. Name Mandatory" then
GenJournalBatch.Get(ConsolidationProcess."Journal Template Name", ConsolidationProcess."Journal Batch Name");
Consolidate.SetDocNo(ConsolidationProcess."Document No.");
if GeneralLedgerSetup."Journal Templ. Name Mandatory" then
Consolidate.SetGenJnlBatch(GenJournalBatch);
SelectedDimension.SetRange("User ID", UserId());
SelectedDimension.SetRange("Object Type", 3);
SelectedDimension.SetRange("Object ID", REPORT::"Import Consolidation from DB");
Consolidate.SetSelectedDim(SelectedDimension);
ValidateNoPostingsAtClosingDates := ConsolidationProcess."Starting Date" = NormalDate(ConsolidationProcess."Starting Date");
if not VerifySubsidiaryConsent(GLSetupJsonObject) then begin
Session.LogSecurityAudit(
FinancialConsolidationServiceNameTxt, SecurityOperationResult::Failure,
StrSubstNo(SecurityAuditConsentMissingTxt, BusinessUnit.Code),
AuditCategory::CustomerFacing);
Error(BusinessUnitConsentErr, BusinessUnit.Code);
end;
GetAndSetPostingGLAccounts(ValidateNoPostingsAtClosingDates, ConsolidationProcess."Starting Date", ConsolidationProcess."Ending Date", Consolidate);
GetAndSetGLEntriesForGLAccountsToConsolidate(ConsolidationProcess."Starting Date", ConsolidationProcess."Ending Date", ConsolidationProcess."Dimensions to Transfer", Consolidate);
if (BusinessUnit."Currency Code" <> '') and (BusinessUnit."Currency Exchange Rate Table" = BusinessUnit."Currency Exchange Rate Table"::"Business Unit") then
CurrencyCode := GetAndSetCurrencyExchangeRates(GLSetupJsonObject, ConsolidationProcess."Parent Currency Code", BusinessUnit."Currency Code", ConsolidationProcess."Ending Date", BusinessUnit."Currency Exchange Rate Table", AdditionalReportingCurrencyCode, Consolidate);
Consolidate.SetGlobals('', '', CopyStr(BusinessUnit."External Company Name", 1, 30), CurrencyCode, AdditionalReportingCurrencyCode, ConsolidationProcess."Parent Currency Code", 0, ConsolidationProcess."Starting Date", ConsolidationProcess."Ending Date");
Consolidate.UpdateGLEntryDimSetID();
BusUnitConsolidationData.SetConsolidate(Consolidate);
end;
local procedure VerifySubsidiaryConsent(): Boolean
var
GLSetupJsonObject: JsonObject;
begin
exit(VerifySubsidiaryConsent(GLSetupJsonObject));
end;
local procedure VerifySubsidiaryConsent(var GLSetupJsonObject: JsonObject): Boolean
var
JsonToken: JsonToken;
begin
GLSetupJsonObject := GetGLSetup();
GLSetupJsonObject.Get('allowQueryFromConsolidation', JsonToken);
exit(JsonToken.AsValue().AsBoolean());
end;
local procedure FormatDateForAPICall(Date: Date): Text
begin
exit(Format(Date, 10, '<Year4>-<Month,2>-<Day,2>'));
end;
local procedure GetAndSetCurrencyExchangeRates(var GLSetupJsonObject: JsonObject; ParentCurrencyCode: Code[10]; BusinessUnitCurrencyCode: Code[10]; EndingDate: Date; CurrencyExchangeRateTable: Option; var AdditionalReportingCurrencyCode: Code[10]; var Consolidate: Codeunit Consolidate): Code[10]
var
BusinessUnit: Record "Business Unit";
CurrencyExchangeRate: Record "Currency Exchange Rate";
JsonToken: JsonToken;
PropertyJsonToken: JsonToken;
CurrencyFilter: Text;
CurrencyCode: Code[10];
LocalCurrencyCode: Code[10];
begin
GLSetupJsonObject.Get('additionalReportingCurrency', JsonToken);
AdditionalReportingCurrencyCode := CopyStr(JsonToken.AsValue().AsCode(), 1, MaxStrLen(AdditionalReportingCurrencyCode));
GLSetupJsonObject.Get('localCurrencyCode', JsonToken);
LocalCurrencyCode := CopyStr(JsonToken.AsValue().AsCode(), 1, MaxStrLen(LocalCurrencyCode));
if (ParentCurrencyCode = '') and (AdditionalReportingCurrencyCode = '') then
exit('');
if LocalCurrencyCode <> '' then
CurrencyCode := LocalCurrencyCode
else
CurrencyCode := BusinessUnitCurrencyCode;
if ParentCurrencyCode <> '' then
CurrencyFilter += StrSubstNo(CurrencyCodeFilterTok, ParentCurrencyCode);
if AdditionalReportingCurrencyCode <> '' then begin
if CurrencyFilter <> '' then
CurrencyFilter += ' or ';
CurrencyFilter += StrSubstNo(CurrencyCodeFilterTok, AdditionalReportingCurrencyCode);
end;
if CurrencyExchangeRateTable = BusinessUnit."Currency Exchange Rate Table"::Local then
exit(CurrencyCode);
foreach JsonToken in GetCurrencyExchangeRates(CurrencyFilter, EndingDate) do begin
JsonToken.AsObject().Get('currencyCode', PropertyJsonToken);
CurrencyExchangeRate."Currency Code" := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(CurrencyExchangeRate."Currency Code"));
JsonToken.AsObject().Get('startingDate', PropertyJsonToken);
CurrencyExchangeRate."Starting Date" := PropertyJsonToken.AsValue().AsDate();
JsonToken.AsObject().Get('relationalCurrencyCode', PropertyJsonToken);
CurrencyExchangeRate."Relational Currency Code" := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(CurrencyExchangeRate."Relational Currency Code"));
JsonToken.AsObject().Get('exchangeRateAmount', PropertyJsonToken);
CurrencyExchangeRate."Exchange Rate Amount" := PropertyJsonToken.AsValue().AsDecimal();
JsonToken.AsObject().Get('relationalExchangeRateAmount', PropertyJsonToken);
CurrencyExchangeRate."Relational Exch. Rate Amount" := PropertyJsonToken.AsValue().AsDecimal();
Consolidate.InsertExchRate(CurrencyExchangeRate);
end;
exit(CurrencyCode);
end;
local procedure GetAndSetGLEntriesForGLAccountsToConsolidate(StartingDate: Date; EndingDate: Date; DimensionsToTransfer: Text; var Consolidate: Codeunit Consolidate)
var
TempGLAccount: Record "G/L Account" temporary;
AccountNoFilter: Text;
DateFilter: Text;
BatchCount: Integer;
GLAccountBatchLimit: Integer;
First: Boolean;
begin
// GLEntries are obtained from the GL Accounts of interest. Each request to GLEntries filters by a maximum of GLAccountBatchLimit GL Accounts
GLAccountBatchLimit := 60;
// If the starting date is a closing date then the ending date will be the same closing date
if StartingDate = ClosingDate(StartingDate) then
DateFilter := StrSubstNo(PostingClosingDateFilterTok, FormatDateForAPICall(StartingDate), FormatDateForAPICall(CalcDate('<+1D>', EndingDate)))
else
DateFilter := StrSubstNo(PostingDateFilterTok, FormatDateForAPICall(StartingDate), FormatDateForAPICall(EndingDate));
Consolidate.GetGLAccounts(TempGLAccount);
First := true;
if not TempGLAccount.FindSet() then
exit;
repeat
if First then
First := false
else
AccountNoFilter += ' or ';
AccountNoFilter += GLAccountToFilter(TempGLAccount);
BatchCount += 1;
if BatchCount >= GLAccountBatchLimit then begin
GetAndSetGLEntries(AccountNoFilter, DateFilter, DimensionsToTransfer, Consolidate);
AccountNoFilter := '';
First := true;
BatchCount := 0;
end;
until TempGLAccount.Next() = 0;
if BatchCount > 0 then
GetAndSetGLEntries(AccountNoFilter, DateFilter, DimensionsToTransfer, Consolidate);
end;
local procedure GetAndSetGLEntries(AccountNoFilter: Text; DateFilter: Text; DimensionsToTransfer: Text; var Consolidate: Codeunit Consolidate)
var
TempDimensionInOtherCompany: Record Dimension temporary;
GLEntry: Record "G/L Entry";
TempDimensionBuffer: Record "Dimension Buffer" temporary;
DimCodeInOtherCompany, DimConsolidationCode : Code[20];
JsonArray: JsonArray;
JsonToken: JsonToken;
PropertyJsonToken: JsonToken;
DimJsonToken: JsonToken;
DimPropertyJsonToken: JsonToken;
GLEntryNo: Integer;
begin
JsonArray := GetGLEntries(DateFilter, AccountNoFilter);
if JsonArray.Count() = 0 then
exit;
if DimensionsToTransfer <> '' then
GetDimensionsFromOtherCompanyMatchingSelected(DimensionsToTransfer, TempDimensionInOtherCompany);
foreach JsonToken in JsonArray do begin
JsonToken.AsObject().Get('entryNumber', PropertyJsonToken);
GLEntry."Entry No." := PropertyJsonToken.AsValue().AsInteger();
JsonToken.AsObject().Get('accountNumber', PropertyJsonToken);
GLEntry."G/L Account No." := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(GLEntry."G/L Account No."));
JsonToken.AsObject().Get('postingDate', PropertyJsonToken);
GLEntry."Posting Date" := PropertyJsonToken.AsValue().AsDate();
JsonToken.AsObject().Get('debitAmount', PropertyJsonToken);
GLEntry."Debit Amount" := PropertyJsonToken.AsValue().AsDecimal();
JsonToken.AsObject().Get('creditAmount', PropertyJsonToken);
GLEntry."Credit Amount" := PropertyJsonToken.AsValue().AsDecimal();
JsonToken.AsObject().Get('additionalCurrencyDebitAmount', PropertyJsonToken);
GLEntry."Add.-Currency Debit Amount" := PropertyJsonToken.AsValue().AsDecimal();
JsonToken.AsObject().Get('additionalCurrencyCreditAmount', PropertyJsonToken);
GLEntry."Add.-Currency Credit Amount" := PropertyJsonToken.AsValue().AsDecimal();
GLEntryNo := Consolidate.InsertGLEntry(GLEntry);
JsonToken.AsObject().Get('dimensionSetLines', PropertyJsonToken);
TempDimensionBuffer."Table ID" := Database::"G/L Entry";
TempDimensionBuffer."Entry No." := GLEntryNo;
foreach DimJsonToken in PropertyJsonToken.AsArray() do begin
DimJsonToken.AsObject().Get('code', DimPropertyJsonToken);
DimCodeInOtherCompany := CopyStr(DimPropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(TempDimensionBuffer."Dimension Code"));
DimJsonToken.AsObject().Get('consolidationCode', DimPropertyJsonToken);
DimConsolidationCode := CopyStr(DimPropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(DimConsolidationCode));
if DimConsolidationCode <> '' then
TempDimensionBuffer."Dimension Code" := DimConsolidationCode
else
TempDimensionBuffer."Dimension Code" := DimCodeInOtherCompany;
DimJsonToken.AsObject().Get('valueCode', DimPropertyJsonToken);
TempDimensionBuffer."Dimension Value Code" := CopyStr(DimPropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(TempDimensionBuffer."Dimension Value Code"));
DimJsonToken.AsObject().Get('valueConsolidationCode', DimPropertyJsonToken);
DimConsolidationCode := CopyStr(DimPropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(DimConsolidationCode));
if DimConsolidationCode <> '' then
TempDimensionBuffer."Dimension Value Code" := DimConsolidationCode;
if TempDimensionInOtherCompany.Get(DimCodeInOtherCompany) then
Consolidate.InsertEntryDim(TempDimensionBuffer, GLEntryNo)
end;
end;
end;
local procedure GetDimensionsFromOtherCompanyMatchingSelected(DimensionsToTransfer: Text; var DimensionInOtherCompany: Record Dimension temporary)
var
DimensionFilter: Text;
begin
DimensionFilter := GetDimensionsRequestFilter(DimensionsToTransfer, DimensionCodeFilterTok);
GetDimensionsFromOtherCompany(DimensionFilter, DimensionInOtherCompany);
DimensionFilter := GetDimensionsRequestFilter(DimensionsToTransfer, DimensionConsolidationCodeFilterTok);
GetDimensionsFromOtherCompany(DimensionFilter, DimensionInOtherCompany);
// https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/webservices/odata-known-limitations#filters
end;
local procedure GetDimensionsRequestFilter(DimensionsToTransfer: Text; FieldFilter: Text): Text
var
DimensionCodeInConsolidationCompany, DimensionFilter : Text;
First: Boolean;
begin
First := true;
foreach DimensionCodeInConsolidationCompany in DimensionsToTransfer.Split(';') do begin
if not First then
DimensionFilter += ' or '
else
First := false;
DimensionFilter += StrSubstNo(FieldFilter, DimensionCodeInConsolidationCompany);
end;
exit(DimensionFilter);
end;
local procedure GetDimensionsFromOtherCompany(Filter: Text; var DimensionInOtherCompany: Record Dimension temporary)
var
JsonToken: JsonToken;
PropertyJsonToken: JsonToken;
DimensionCode: Code[20];
begin
foreach JsonToken in GetDimensions(Filter) do begin
JsonToken.AsObject().Get('code', PropertyJsonToken);
DimensionCode := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(DimensionCode));
if not DimensionInOtherCompany.Get(DimensionCode) then begin
DimensionInOtherCompany.Code := DimensionCode;
DimensionInOtherCompany.Insert();
end;
end;
end;
local procedure GLAccountToFilter(TempGLAccount: Record "G/L Account" temporary): Text
begin
exit(GLAccountToFilter(TempGLAccount."No."));
end;
local procedure GLAccountToFilter(GLAccountNo: Code[20]): Text
var
TypeHelper: Codeunit "Type Helper";
URLEncodedGLAccountNo: Text;
begin
URLEncodedGLAccountNo := GLAccountNo;
TypeHelper.UrlEncode(URLEncodedGLAccountNo);
exit(StrSubstNo(GLAccountFilterTok, URLEncodedGLAccountNo));
end;
local procedure GetAndSetPostingGLAccounts(ValidateNoPostingsAtClosingDates: Boolean; StartingDate: Date; EndingDate: Date; var Consolidate: Codeunit Consolidate)
var
GLAccount: Record "G/L Account";
TempAccountingPeriod: Record "Accounting Period" temporary;
TypeHelper: Codeunit "Type Helper";
RecordRef: RecordRef;
FieldRef: FieldRef;
JsonToken: JsonToken;
PropertyJsonToken: JsonToken;
begin
if ValidateNoPostingsAtClosingDates then
foreach JsonToken in GetAccountingPeriods(StartingDate, EndingDate) do begin
JsonToken.AsObject().Get('startingDate', PropertyJsonToken);
TempAccountingPeriod."Starting Date" := PropertyJsonToken.AsValue().AsDate();
TempAccountingPeriod.Insert();
end;
RecordRef.Open(Database::"G/L Account");
FieldRef := RecordRef.Field(GLAccount.FieldNo("Consol. Translation Method"));
foreach JsonToken in GetPostingGLAccounts() do begin
JsonToken.AsObject().Get('number', PropertyJsonToken);
GLAccount."No." := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(GLAccount."No."));
if ValidateNoPostingsAtClosingDates then
CheckNoPostingsAtClosingDates(GLAccount."No.", TempAccountingPeriod);
JsonToken.AsObject().Get('consolidationTranslationMethod', PropertyJsonToken);
GLAccount."Consol. Translation Method" := TypeHelper.GetOptionNo(PropertyJsonToken.AsValue().AsText(), FieldRef.OptionMembers);
JsonToken.AsObject().Get('consolidationDebitAccount', PropertyJsonToken);
GLAccount."Consol. Debit Acc." := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(GLAccount."Consol. Debit Acc."));
JsonToken.AsObject().Get('consolidationCreditAccount', PropertyJsonToken);
GLAccount."Consol. Credit Acc." := CopyStr(PropertyJsonToken.AsValue().AsCode(), 1, MaxStrLen(GLAccount."Consol. Credit Acc."));
Consolidate.InsertGLAccount(GLAccount);
end;
end;
local procedure CheckNoPostingsAtClosingDates(GLAccountNo: Code[20]; var TempAccountingPeriod: Record "Accounting Period" temporary)
var
ClosingDate: Date;
EntriesAtClosingDates: Integer;
begin
if not TempAccountingPeriod.FindSet() then
exit;
repeat
ClosingDate := TempAccountingPeriod."Starting Date" - 1;
EntriesAtClosingDates := GetGLEntriesCountAtDate(GLAccountNo, ClosingDate);
if EntriesAtClosingDates <> 0 then
Error(EntriesAtClosingDateErr, ClosingDate, GLAccountNo);
until TempAccountingPeriod.Next() = 0;
end;
local procedure GetGLSetup(): JsonObject
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetGLSetup(IsHandled, Response);
if IsHandled then
exit(ParseDataSingle(Response));
exit(HttpGetSingle(BusinessUnitAPIBaseUrl + '/generalLedgerSetup'));
end;
local procedure GetCurrencyExchangeRates(CurrencyFilter: Text; EndingDate: Date): JsonArray
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetCurrencyExchangeRates(CurrencyFilter, EndingDate, IsHandled, Response);
if IsHandled then
exit(ParseData(Response));
exit(HttpGet(BusinessUnitAPIBaseUrl + CurrencyExchangeRatesSegmentTok + '?$filter=(' + CurrencyFilter + ') and startingDate le ' + FormatDateForAPICall(EndingDate)));
end;
local procedure GetGLEntries(DateFilter: Text; AccountNoFilter: Text): JsonArray
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetGLEntries(DateFilter, AccountNoFilter, IsHandled, Response);
if IsHandled then
exit(ParseData(Response));
exit(HttpGet(BusinessUnitAPIBaseUrl + '/generalLedgerEntries?$filter=' + DateFilter + ' and (' + AccountNoFilter + ')&$expand=dimensionSetLines', true));
end;
local procedure GetDimensions(DimensionFilter: Text): JsonArray
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetDimensions(DimensionFilter, IsHandled, Response);
if IsHandled then
exit(ParseData(Response));
exit(HttpGet(BusinessUnitAPIBaseUrl + '/dimensions?$filter=' + DimensionFilter));
end;
local procedure GetGLEntriesCountAtDate(GLAccountNo: Code[20]; ClosingDate: Date): Integer
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetGLEntriesCountAtDate(GLAccountNo, ClosingDate, IsHandled, Response);
if IsHandled then
exit(ParseCount(Response));
exit(HttpGetCount(BusinessUnitAPIBaseUrl + '/generalLedgerEntries?$filter=' + GLAccountToFilter(GLAccountNo) + ' and postingDate eq ' + FormatDateForAPICall(ClosingDate)));
end;
local procedure GetPostingGLAccounts(): JsonArray
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetPostingGLAccounts(IsHandled, Response);
if IsHandled then
exit(ParseData(Response));
exit(HttpGet(BusinessUnitAPIBaseUrl + '/accounts?$filter=accountType eq ''Posting'' and excludeFromConsolidation eq false'));
end;
local procedure GetAccountingPeriods(StartingDate: Date; EndingDate: Date): JsonArray
var
Response: Text;
IsHandled: Boolean;
begin
OnBeforeGetAccountingPeriods(StartingDate, EndingDate, IsHandled, Response);
if IsHandled then
exit(ParseData(Response));
exit(HttpGet(BusinessUnitAPIBaseUrl + '/accountingPeriods?$filter=newFiscalYear eq true and dateLocked eq true and startingDate ge ' + FormatDateForAPICall(StartingDate + 1) + ' and startingDate le ' + FormatDateForAPICall(EndingDate)));
end;
local procedure ParseData(Response: Text): JsonArray
var
JsonObject: JsonObject;
JsonToken: JsonToken;
begin
JsonObject.ReadFrom(Response);
JsonObject.Get('value', JsonToken);
exit(JsonToken.AsArray());
end;
local procedure ParseDataSingle(Response: Text): JsonObject
var
JsonArray: JsonArray;
JsonToken: JsonToken;
begin
JsonArray := ParseData(Response);
if JsonArray.Count() <> 1 then
Error(APIGetSingleGotOtherErr, JsonArray.Count());
JsonArray.Get(0, JsonToken);
exit(JsonToken.AsObject());
end;
local procedure ParseCount(Response: Text): Integer
var
JsonObject: JsonObject;
JsonToken: JsonToken;
begin
JsonObject.ReadFrom(Response);
JsonObject.Get('@odata.count', JsonToken);
exit(JsonToken.AsValue().AsInteger());
end;
local procedure HttpGetCount(Uri: Text): Integer
begin
exit(ParseCount(HttpGetTextWithStatusHandling(Uri + '&$count=true&$top=0')));
end;
local procedure HttpGet(Uri: Text): JsonArray
begin
exit(HttpGet(Uri, false));
end;
local procedure HttpGet(Uri: Text; Paginate: Boolean): JsonArray
var
TotalJsonArray: JsonArray;
JsonToken: JsonToken;
UriWithPagination: Text;
RecordsRead, Count : Integer;
begin
if Paginate then begin
Count := HttpGetCount(Uri);
RecordsRead := 0;
while RecordsRead < Count do begin
UriWithPagination := Uri + '&$skip=' + Format(RecordsRead) + '&$top=' + Format(ConsolidationSetup.PageSize);
foreach JsonToken in ParseData(HttpGetTextWithStatusHandling(UriWithPagination)) do
TotalJsonArray.Add(JsonToken);
RecordsRead += ConsolidationSetup.PageSize;
end;
exit(TotalJsonArray);
end
else
exit(ParseData(HttpGetTextWithStatusHandling(Uri)));
end;
local procedure HttpGetSingle(Uri: Text): JsonObject
begin
exit(ParseDataSingle(HttpGetTextWithStatusHandling(Uri)));
end;
/// <summary>
/// Integration event raised before retrieving General Ledger Setup from external Business Central API.
/// Enables custom G/L setup retrieval or bypassing of standard API calls.
/// </summary>
/// <param name="IsHandled">Set to true to bypass standard G/L setup API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetGLSetup(var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving currency exchange rates from external Business Central API.
/// Enables custom exchange rate retrieval or modification of API request parameters.
/// </summary>
/// <param name="CurrencyFilter">Currency filter applied to exchange rate API request</param>
/// <param name="EndingDate">End date for exchange rate retrieval period</param>
/// <param name="IsHandled">Set to true to bypass standard exchange rate API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetCurrencyExchangeRates(CurrencyFilter: Text; EndingDate: Date; var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving G/L entries from external Business Central API.
/// Enables custom G/L entry retrieval or modification of API request filters.
/// </summary>
/// <param name="DateFilter">Date filter applied to G/L entries API request</param>
/// <param name="AccountNoFilter">Account number filter for G/L entries retrieval</param>
/// <param name="IsHandled">Set to true to bypass standard G/L entries API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetGLEntries(DateFilter: Text; AccountNoFilter: Text; var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving dimensions from external Business Central API.
/// Enables custom dimension retrieval or modification of dimension filter parameters.
/// </summary>
/// <param name="DimensionFilter">Dimension filter applied to dimensions API request</param>
/// <param name="IsHandled">Set to true to bypass standard dimensions API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetDimensions(DimensionFilter: Text; var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving G/L entries count at specific date from external Business Central API.
/// Enables custom entry count retrieval or bypassing of standard API calls for closing date entries.
/// </summary>
/// <param name="GLAccountNo">G/L Account number to count entries for</param>
/// <param name="ClosingDate">Closing date for entry count calculation</param>
/// <param name="IsHandled">Set to true to bypass standard G/L entry count API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetGLEntriesCountAtDate(GLAccountNo: Code[20]; ClosingDate: Date; var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving accounting periods from external Business Central API.
/// Enables custom accounting period retrieval or modification of date range parameters.
/// </summary>
/// <param name="StartingDate">Starting date for accounting periods retrieval</param>
/// <param name="EndingDate">Ending date for accounting periods retrieval</param>
/// <param name="IsHandled">Set to true to bypass standard accounting periods API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetAccountingPeriods(StartingDate: Date; EndingDate: Date; var IsHandled: Boolean; var Response: Text)
begin
end;
/// <summary>
/// Integration event raised before retrieving posting G/L accounts from external Business Central API.
/// Enables custom G/L account retrieval for posting account filtering during consolidation.
/// </summary>
/// <param name="IsHandled">Set to true to bypass standard posting G/L accounts API retrieval</param>
/// <param name="Response">Custom API response text when IsHandled is true</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeGetPostingGLAccounts(var IsHandled: Boolean; var Response: Text)
begin
end;
}