Codeunit 850 Cash Flow Forecast Handler
- App
- Base Application
- Namespace
- Microsoft.CashFlow.Forecast
- Versions
- 17-28
Versions171819202122232425262728
Source29
Source in 29
src/Layers/W1/BaseApp/CashFlow/Forecast/CashFlowForecastHandler.Codeunit.al765 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.CashFlow.Forecast;
using Microsoft.CashFlow.Setup;
using Microsoft.Finance.VAT.Ledger;
using Microsoft.Purchases.Document;
using Microsoft.Purchases.Payables;
using Microsoft.Sales.Document;
using Microsoft.Sales.Receivables;
using System.AI;
using System.Environment;
using System.Utilities;
using System.Visualization;
codeunit 850 "Cash Flow Forecast Handler"
{
Permissions = TableData "Cash Flow Azure AI Buffer" = rimd;
trigger OnRun()
begin
end;
var
CashFlowSetup: Record "Cash Flow Setup";
ErrorMessage: Record "Error Message";
TempErrorMessage: Record "Error Message" temporary;
CashFlowManagement: Codeunit "Cash Flow Management";
TimeSeriesManagement: Codeunit "Time Series Management";
ClientTypeManagement: Codeunit "Client Type Management";
NumberOfPeriodsWithHistory: Integer;
PeriodType: Integer;
ForecastStartDate: Date;
XPAYABLESTxt: Label 'PAYABLES', Locked = true;
XRECEIVABLESTxt: Label 'RECEIVABLES', Locked = true;
XPAYABLESCORRECTIONTxt: Label 'Payables Correction';
XRECEIVABLESCORRECTIONTxt: Label 'Receivables Correction';
XTAXPAYABLESTxt: Label 'TAX TO RETURN', Locked = true;
XTAXRECEIVABLESTxt: Label 'TAX TO PAY', Locked = true;
XTAXPAYABLESCORRECTIONTxt: Label 'Tax from Purchase entries';
XTAXRECEIVABLESCORRECTIONTxt: Label 'Tax from Sales entries';
XTAXSALESORDERSTxt: Label 'Tax from Sales Orders';
XTAXPURCHORDERSTxt: Label 'Tax from Purchase Orders';
XSALESORDERSTxt: Label 'Sales Orders';
XPURCHORDERSTxt: Label 'Purchase Orders';
AzureAIMustBeEnabledErr: Label '%1 in %2 must be set to true.', Comment = '%1 =Azure AI Enabled field, %2-Cash Flow Setup';
AzureAIAPIURLEmptyErr: Label 'You must specify an %1 and an %2 for the %3.', Comment = '%1 =API URL field,%2 =API Key field, %3-Cash Flow Setup';
AzureMachineLearningLimitReachedErr: Label 'The Microsoft Azure Machine Learning limit has been reached. Please contact your system administrator.';
TimeSeriesManagementInitFailedErr: Label 'Cannot initialize Microsoft Azure Machine Learning. Try again later. If the problem continues, contact your system administrator.';
MinimumHistoricalDataErr: Label 'There is not enough historical data for Azure AI to create a forecast.';
PredictionHasHighVarianceErr: Label 'The calculated forecast for %1 for the period from %2 shows a degree of variance that is higher than the setup allows.', Comment = '%1 =PAYABLES or RECEIVABLES,%2 =Date';
SetupScheduledForecastingMsg: Label 'You can include Azure AI capabilities in the cash flow forecast.';
EnableAzureAITxt: Label 'Enable Azure AI';
DontAskAgainTxt: Label 'Don''t ask again';
ScheduledForecastingEnabledMsg: Label 'The Azure AI forecast has been enabled.', Comment = '%1 = weekday (e.g. Monday)';
procedure CalculateForecast(): Boolean
begin
TempErrorMessage.ClearLog();
if not Initialize() then begin
ErrorMessage.CopyFromTemp(TempErrorMessage);
Commit();
exit(false);
end;
CalculateVATAndLedgerForecast();
Commit();
exit(true);
end;
local procedure CalculateVATAndLedgerForecast(): Boolean
var
TempTimeSeriesForecast: Record "Time Series Forecast" temporary;
TempTimeSeriesBuffer: Record "Time Series Buffer" temporary;
begin
// Get forecast using time series
if not PrepareForecast(TempTimeSeriesBuffer) then begin
ErrorMessage.CopyFromTemp(TempErrorMessage);
Commit();
exit(false);
end;
TimeSeriesManagement.SetPreparedData(TempTimeSeriesBuffer, PeriodType, ForecastStartDate, NumberOfPeriodsWithHistory);
TimeSeriesManagement.Forecast(CashFlowSetup.Horizon, 80, CashFlowSetup."Time Series Model");
TimeSeriesManagement.GetForecast(TempTimeSeriesForecast);
// Insert forecasted data
ClearCashFlowAzureAIBuffer();
CashFlowAzureAIBufferFill(TempTimeSeriesBuffer, TempTimeSeriesForecast);
ErrorMessage.CopyFromTemp(TempErrorMessage);
end;
local procedure CalculatePostedSalesDocsSumAmountInPeriod(StartingDate: Date; PeriodType: Option): Decimal
var
CustLedgerEntry: Record "Cust. Ledger Entry";
Total: Decimal;
begin
Total := 0;
CustLedgerEntry.SetCurrentKey("Due Date");
CustLedgerEntry.SetRange("Due Date", StartingDate, GetPeriodEndingDate(StartingDate, PeriodType));
if CustLedgerEntry.FindSet() then
repeat
CustLedgerEntry.CalcFields("Amount (LCY)");
Total := Total + CustLedgerEntry."Amount (LCY)";
until CustLedgerEntry.Next() = 0;
exit(Abs(Round(Total, 0.01)));
end;
local procedure CalculatePostedPurchDocsSumAmountInPeriod(StartingDate: Date; PeriodType: Option): Decimal
var
VendorLedgerEntry: Record "Vendor Ledger Entry";
Total: Decimal;
begin
Total := 0;
VendorLedgerEntry.SetCurrentKey("Due Date");
VendorLedgerEntry.SetRange("Due Date", StartingDate, GetPeriodEndingDate(StartingDate, PeriodType));
if VendorLedgerEntry.FindSet() then
repeat
VendorLedgerEntry.CalcFields("Amount (LCY)");
Total := Total + VendorLedgerEntry."Amount (LCY)";
until VendorLedgerEntry.Next() = 0;
exit(Abs(Round(Total, 0.01)));
end;
local procedure CalculatePostedDocsSumTaxInPeriod(EndingDate: Date; IsSales: Boolean): Decimal
var
VATEntry: Record "VAT Entry";
Total: Decimal;
StartDate: Date;
EndDate: Date;
begin
Total := 0;
VATEntry.SetCurrentKey("Document Date");
CashFlowSetup.GetTaxPeriodStartEndDates(EndingDate, StartDate, EndDate);
VATEntry.SetRange("Document Date", StartDate, EndDate);
if IsSales then
VATEntry.SetRange(Type, VATEntry.Type::Sale)
else
VATEntry.SetRange(Type, VATEntry.Type::Purchase);
if VATEntry.FindSet() then
repeat
Total := Total + VATEntry.Amount;
until VATEntry.Next() = 0;
exit(Abs(Round(Total, 0.01)));
end;
local procedure CalculateNotPostedSalesOrderSumAmountInPeriod(StartingDate: Date; PeriodType: Option; IsTax: Boolean): Decimal
var
SalesHeader: Record "Sales Header";
TaxEndingDate: Date;
TaxStartingDate: Date;
Total: Decimal;
AmountValue: Decimal;
begin
Total := 0;
// in case of tax starting Date is tax payment due Date
if IsTax then begin
SalesHeader.SetCurrentKey("Document Date");
CashFlowSetup.GetTaxPeriodStartEndDates(StartingDate, TaxStartingDate, TaxEndingDate);
SalesHeader.SetRange("Document Date", TaxStartingDate, TaxEndingDate)
end else begin
SalesHeader.SetCurrentKey("Due Date");
SalesHeader.SetRange("Due Date", StartingDate, GetPeriodEndingDate(StartingDate, PeriodType));
end;
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
if SalesHeader.FindSet() then
repeat
if IsTax then
AmountValue := -CashFlowManagement.GetTaxAmountFromSalesOrder(SalesHeader)
else
AmountValue := CashFlowManagement.GetTotalAmountFromSalesOrder(SalesHeader);
Total := Total + AmountValue;
until SalesHeader.Next() = 0;
if Total < 0 then
Total := 0;
exit(Abs(Round(Total, 0.01)));
end;
local procedure CalculateNotPostedPurchOrderSumAmountInPeriod(StartingDate: Date; PeriodType: Option; IsTax: Boolean): Decimal
var
PurchaseHeader: Record "Purchase Header";
Total: Decimal;
AmountValue: Decimal;
TaxEndingDate: Date;
TaxStartingDate: Date;
begin
Total := 0;
PurchaseHeader.SetCurrentKey("Due Date");
// in case of tax starting Date is tax payment due Date
if IsTax then begin
PurchaseHeader.SetCurrentKey("Document Date");
CashFlowSetup.GetTaxPeriodStartEndDates(StartingDate, TaxStartingDate, TaxEndingDate);
PurchaseHeader.SetRange("Document Date", TaxStartingDate, TaxEndingDate)
end else begin
PurchaseHeader.SetCurrentKey("Due Date");
PurchaseHeader.SetRange("Due Date", StartingDate, GetPeriodEndingDate(StartingDate, PeriodType));
end;
PurchaseHeader.SetRange("Document Type", PurchaseHeader."Document Type"::Order);
if PurchaseHeader.FindSet() then
repeat
if IsTax then
AmountValue := CashFlowManagement.GetTaxAmountFromPurchaseOrder(PurchaseHeader)
else
AmountValue := CashFlowManagement.GetTotalAmountFromPurchaseOrder(PurchaseHeader);
Total := Total - AmountValue;
until PurchaseHeader.Next() = 0;
if Total > 0 then
Total := 0;
exit(Abs(Round(Total, 0.01)));
end;
procedure PrepareForecast(var TimeSeriesBuffer: Record "Time Series Buffer"): Boolean
var
CustLedgerEntry: Record "Cust. Ledger Entry";
VendorLedgerEntry: Record "Vendor Ledger Entry";
TempCustTimeSeriesBuffer: Record "Time Series Buffer" temporary;
TempVendTimeSeriesBuffer: Record "Time Series Buffer" temporary;
VATEntry: Record "VAT Entry";
TempVATTimeSeriesBuffer: Record "Time Series Buffer" temporary;
HasMinimumHistory: Boolean;
HasMinimumHistoryLoc: Boolean;
NumberOfPeriodsWithHistoryLoc: Integer;
begin
CashFlowSetup.Get();
PeriodType := CashFlowSetup."Period Type";
SetFiltersOnRecords(CustLedgerEntry, VendorLedgerEntry);
// check if there is a minimum history needed for forecast
HasMinimumHistory := TimeSeriesManagement.HasMinimumHistoricalData(
NumberOfPeriodsWithHistory,
CustLedgerEntry,
CustLedgerEntry.FieldNo("Due Date"),
CashFlowSetup."Period Type",
ForecastStartDate);
OnAfterHasMinimumHistoricalData(
HasMinimumHistoryLoc,
NumberOfPeriodsWithHistoryLoc,
CashFlowSetup."Period Type",
ForecastStartDate);
HasMinimumHistory := (HasMinimumHistory or HasMinimumHistoryLoc);
if not HasMinimumHistory then begin
TempErrorMessage.LogSimpleMessage(ErrorMessage."Message Type"::Error, MinimumHistoricalDataErr);
exit(false);
end;
if NumberOfPeriodsWithHistoryLoc > NumberOfPeriodsWithHistory then
NumberOfPeriodsWithHistory := NumberOfPeriodsWithHistoryLoc;
// Prepare Sales History
PrepareData(
TempCustTimeSeriesBuffer,
CustLedgerEntry,
CustLedgerEntry.FieldNo("Document Type"),
CustLedgerEntry.FieldNo("Due Date"),
CustLedgerEntry.FieldNo("Amount (LCY)"),
Format(CustLedgerEntry."Document Type"::Invoice),
Format(CustLedgerEntry."Document Type"::"Credit Memo")
);
OnAfterPrepareSalesHistoryData(TempCustTimeSeriesBuffer, PeriodType, ForecastStartDate, NumberOfPeriodsWithHistory);
AppendRecords(TimeSeriesBuffer, TempCustTimeSeriesBuffer, XRECEIVABLESTxt);
// Prepare Purchase History
PrepareData(
TempVendTimeSeriesBuffer,
VendorLedgerEntry,
VendorLedgerEntry.FieldNo("Document Type"),
VendorLedgerEntry.FieldNo("Due Date"),
VendorLedgerEntry.FieldNo("Amount (LCY)"),
Format(VendorLedgerEntry."Document Type"::Invoice),
Format(VendorLedgerEntry."Document Type"::"Credit Memo")
);
OnAfterPreparePurchHistoryData(TempVendTimeSeriesBuffer, PeriodType, ForecastStartDate, NumberOfPeriodsWithHistory);
AppendRecords(TimeSeriesBuffer, TempVendTimeSeriesBuffer, XPAYABLESTxt);
VATEntry.SetCurrentKey("Document Date");
VATEntry.SetRange(Type, VATEntry.Type::Sale);
// Prepare VAT History
TimeSeriesPrepareAndGetData(
TempVATTimeSeriesBuffer,
VATEntry,
VATEntry.FieldNo(Type),
VATEntry.FieldNo("Document Date"),
VATEntry.FieldNo(Amount)
);
AppendRecords(TimeSeriesBuffer, TempVATTimeSeriesBuffer, XTAXRECEIVABLESTxt);
TempVATTimeSeriesBuffer.DeleteAll();
VATEntry.SetRange(Type, VATEntry.Type::Purchase);
// Prepare VAT History
TimeSeriesPrepareAndGetData(
TempVATTimeSeriesBuffer,
VATEntry,
VATEntry.FieldNo(Type),
VATEntry.FieldNo("Document Date"),
VATEntry.FieldNo(Amount)
);
AppendRecords(TimeSeriesBuffer, TempVATTimeSeriesBuffer, XTAXPAYABLESTxt);
exit(true);
end;
procedure CashFlowAzureAIBufferFill(var TimeSeriesBuffer: Record "Time Series Buffer"; var TimeSeriesForecast: Record "Time Series Forecast")
var
CashFlowAzureAIBuffer: Record "Cash Flow Azure AI Buffer";
ForecastedRemainingAmount: Decimal;
begin
CashFlowSetup.Get();
PeriodType := CashFlowSetup."Period Type";
// History Records
if TimeSeriesBuffer.FindSet() then
repeat
NewCashFlowAzureAIBufferRecord(TimeSeriesBuffer."Group ID",
TimeSeriesBuffer.Value,
1,
0,
CashFlowAzureAIBuffer.Type::History,
TimeSeriesBuffer."Period Start Date",
PeriodType,
TimeSeriesBuffer."Period No."
);
until TimeSeriesBuffer.Next() = 0;
AggregateTaxRecordsToTaxablePeriod(TimeSeriesForecast, XTAXPAYABLESTxt);
AggregateTaxRecordsToTaxablePeriod(TimeSeriesForecast, XTAXRECEIVABLESTxt);
// Forecast
if TimeSeriesForecast.FindSet() then
repeat
// if Variance % is big, do not insert it
if TimeSeriesForecast."Delta %" >= CashFlowSetup."Variance %" then
TempErrorMessage.LogSimpleMessage(ErrorMessage."Message Type"::Warning,
StrSubstNo(PredictionHasHighVarianceErr, TimeSeriesForecast."Group ID", TimeSeriesForecast."Period Start Date"))
else
if IsAmountValid(TimeSeriesForecast) then begin
// Azure AI Forecast
ForecastedRemainingAmount := Abs(TimeSeriesForecast.Value);
NewCashFlowAzureAIBufferRecord(TimeSeriesForecast."Group ID",
TimeSeriesForecast.Value,
TimeSeriesForecast."Delta %",
TimeSeriesForecast.Delta,
CashFlowAzureAIBuffer.Type::Forecast,
TimeSeriesForecast."Period Start Date",
PeriodType,
TimeSeriesForecast."Period No."
);
// Corrections: Payables Corrections, Receivables Corrections, Sales Orders, Purchase Orders
case TimeSeriesForecast."Group ID" of
XPAYABLESTxt:
begin
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XPAYABLESCORRECTIONTxt, PeriodType);
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XPURCHORDERSTxt, PeriodType);
end;
XRECEIVABLESTxt:
begin
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XRECEIVABLESCORRECTIONTxt, PeriodType);
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XSALESORDERSTxt, PeriodType);
end;
XTAXPAYABLESTxt:
begin
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XTAXPAYABLESCORRECTIONTxt, PeriodType);
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XTAXPURCHORDERSTxt, PeriodType);
end;
XTAXRECEIVABLESTxt:
begin
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XTAXRECEIVABLESCORRECTIONTxt, PeriodType);
NewCorrectionRecord(ForecastedRemainingAmount, TimeSeriesForecast, XTAXSALESORDERSTxt, PeriodType);
end;
end;
end;
until TimeSeriesForecast.Next() = 0;
end;
local procedure ClearCashFlowAzureAIBuffer()
var
CashFlowAzureAIBuffer: Record "Cash Flow Azure AI Buffer";
begin
CashFlowAzureAIBuffer.Reset();
CashFlowAzureAIBuffer.DeleteAll();
end;
local procedure SetFiltersOnRecords(var CustLedgerEntry: Record "Cust. Ledger Entry"; var VendorLedgerEntry: Record "Vendor Ledger Entry")
begin
CustLedgerEntry.SetCurrentKey("Due Date");
CustLedgerEntry.SetFilter("Document Type",
'%1|%2',
CustLedgerEntry."Document Type"::Invoice,
CustLedgerEntry."Document Type"::"Credit Memo");
VendorLedgerEntry.SetCurrentKey("Due Date");
VendorLedgerEntry.SetFilter("Document Type",
'%1|%2',
VendorLedgerEntry."Document Type"::Invoice,
VendorLedgerEntry."Document Type"::"Credit Memo");
end;
local procedure PrepareData(var TempTimeSeriesBuffer: Record "Time Series Buffer" temporary; RecordVariant: Variant; GroupIdFieldNo: Integer; DateFieldNo: Integer; ValueFieldNo: Integer; InvoiceOption: Text; CreditMemoOption: Text)
var
TempCreditMemoTimeSeriesBuffer: Record "Time Series Buffer" temporary;
begin
TimeSeriesPrepareAndGetData(TempTimeSeriesBuffer, RecordVariant, GroupIdFieldNo, DateFieldNo, ValueFieldNo);
TempCreditMemoTimeSeriesBuffer.Copy(TempTimeSeriesBuffer, true);
TempCreditMemoTimeSeriesBuffer.SetRange("Group ID", CreditMemoOption);
TempTimeSeriesBuffer.SetRange("Group ID", InvoiceOption);
if TempTimeSeriesBuffer.FindSet() and TempCreditMemoTimeSeriesBuffer.FindSet() then
repeat
TempTimeSeriesBuffer.Value := TempTimeSeriesBuffer.Value + TempCreditMemoTimeSeriesBuffer.Value;
TempTimeSeriesBuffer.Modify();
until (TempTimeSeriesBuffer.Next() = 0) and (TempCreditMemoTimeSeriesBuffer.Next() = 0);
end;
local procedure TimeSeriesPrepareAndGetData(var TempTimeSeriesBuffer: Record "Time Series Buffer" temporary; RecordVariant: Variant; GroupIdFieldNo: Integer; DateFieldNo: Integer; ValueFieldNo: Integer)
begin
TimeSeriesManagement.PrepareData(
RecordVariant,
GroupIdFieldNo,
DateFieldNo,
ValueFieldNo,
PeriodType,
ForecastStartDate,
NumberOfPeriodsWithHistory);
TimeSeriesManagement.GetPreparedData(TempTimeSeriesBuffer);
end;
local procedure AppendRecords(var TargetTimeSeriesBuffer: Record "Time Series Buffer"; var SourceTimeSeriesBuffer: Record "Time Series Buffer"; Label: Text[50])
begin
if SourceTimeSeriesBuffer.FindSet() then
repeat
if TargetTimeSeriesBuffer.Get(Label, SourceTimeSeriesBuffer."Period No.") then begin
TargetTimeSeriesBuffer.Validate(Value, (TargetTimeSeriesBuffer.Value + SourceTimeSeriesBuffer.Value));
TargetTimeSeriesBuffer.Modify();
end else begin
TargetTimeSeriesBuffer.Validate(Value, SourceTimeSeriesBuffer.Value);
TargetTimeSeriesBuffer.Validate("Period Start Date", SourceTimeSeriesBuffer."Period Start Date");
TargetTimeSeriesBuffer.Validate("Period No.", SourceTimeSeriesBuffer."Period No.");
TargetTimeSeriesBuffer.Validate("Group ID", Label);
TargetTimeSeriesBuffer.Insert();
end;
until SourceTimeSeriesBuffer.Next() = 0;
end;
[Scope('OnPrem')]
procedure Initialize(): Boolean
var
AzureAIUsage: Codeunit "Azure AI Usage";
AzureAIService: Enum "Azure AI Service";
APIURL: Text[250];
APIKey: SecretText;
LimitValue: Decimal;
IsInitialized: Boolean;
TimeSeriesLibState: Option Uninitialized,Initialized,"Data Prepared",Done;
UsingStandardCredentials: Boolean;
begin
if not CashFlowSetup.Get() then
exit(false);
IsInitialized := true;
// check if Azure AI is enabled
TempErrorMessage.SetContext(CashFlowSetup);
if not CashFlowSetup."Azure AI Enabled" then begin
TempErrorMessage.LogMessage(
CashFlowSetup, CashFlowSetup.FieldNo("Azure AI Enabled"), ErrorMessage."Message Type"::Error,
StrSubstNo(AzureAIMustBeEnabledErr, CashFlowSetup.FieldCaption("Azure AI Enabled"),
CashFlowSetup.TableCaption()));
IsInitialized := false;
end;
// check Azure ML settings
if not CashFlowSetup.GetMLCredentials(APIURL, APIKey, LimitValue, UsingStandardCredentials) then begin
TempErrorMessage.LogMessage(
CashFlowSetup, CashFlowSetup.FieldNo("API URL"), ErrorMessage."Message Type"::Error,
StrSubstNo(AzureAIAPIURLEmptyErr, CashFlowSetup.FieldCaption("API URL"),
CashFlowSetup.FieldCaption("API Key"), CashFlowSetup.TableCaption()));
IsInitialized := false;
end;
if IsInitialized = false then
exit(false);
// check - it will be fixed with Time Series Lib
if not CashFlowSetup.IsAPIUserDefined() then
if AzureAIUsage.IsLimitReached(AzureAIService::"Machine Learning", LimitValue) then begin
TempErrorMessage.LogSimpleMessage(ErrorMessage."Message Type"::Error, AzureMachineLearningLimitReachedErr);
exit(false);
end;
// Time series Lib
if not TimeSeriesManagement.Initialize(APIURL, APIKey, CashFlowSetup.TimeOut, UsingStandardCredentials) then begin
TempErrorMessage.LogSimpleMessage(ErrorMessage."Message Type"::Error, TimeSeriesManagementInitFailedErr);
exit(false);
end;
TimeSeriesManagement.SetMaximumHistoricalPeriods(CashFlowSetup."Historical Periods");
TimeSeriesManagement.GetState(TimeSeriesLibState);
// set defaults
PeriodType := CashFlowSetup."Period Type";
ForecastStartDate := GetForecastStartDate(PeriodType);
exit(true);
end;
local procedure GetPeriodEndingDate(StartingDate: Date; PeriodType: Option Day,Week,Month,Quarter,Year): Date
begin
case PeriodType of
PeriodType::Day:
exit(StartingDate);
PeriodType::Week:
exit(CalcDate('<+1W-1D>', StartingDate));
PeriodType::Month:
exit(CalcDate('<+1M-1D>', StartingDate));
PeriodType::Quarter:
exit(CalcDate('<+1Q-1D>', StartingDate));
PeriodType::Year:
exit(CalcDate('<+1Y-1D>', StartingDate));
end;
end;
local procedure NewCashFlowAzureAIBufferRecord(GroupIdValue: Text[50]; AmountValue: Decimal; DeltaPercentValue: Decimal; DeltaValue: Decimal; TypeValue: Option; PeriodStartValue: Date; PeriodTypeValue: Option; PeriodNoValue: Integer)
var
CashFlowAzureAIBuffer: Record "Cash Flow Azure AI Buffer";
begin
CashFlowAzureAIBuffer.Init();
CashFlowAzureAIBuffer.Validate("Group Id", GroupIdValue);
CashFlowAzureAIBuffer.Validate(Amount, AmountValue);
CashFlowAzureAIBuffer.Validate("Delta %", DeltaPercentValue);
CashFlowAzureAIBuffer.Validate(Delta, DeltaValue);
CashFlowAzureAIBuffer.Validate(Type, TypeValue);
CashFlowAzureAIBuffer.Validate("Period Start", PeriodStartValue);
CashFlowAzureAIBuffer.Validate("Period Type", PeriodTypeValue);
CashFlowAzureAIBuffer.Validate("Period No.", PeriodNoValue);
CashFlowAzureAIBuffer.Insert();
end;
local procedure NewCorrectionRecord(var ForecastedRemainingAmount: Decimal; TimeSeriesForecast: Record "Time Series Forecast"; GroupIDValue: Text[100]; PeriodTypeValue: Option)
var
CashFlowAzureAIBuffer: Record "Cash Flow Azure AI Buffer";
CorrectionAmount: Decimal;
begin
if ForecastedRemainingAmount = 0 then
exit;
// correction must not be bigger than forecasted amount
CorrectionAmount := GetCorrectionAmount(TimeSeriesForecast, GroupIDValue, PeriodTypeValue);
if CorrectionAmount = 0 then
exit;
// if correction amount smaller than forecasted, keep correction and reduce forecasted remaining amount
if ForecastedRemainingAmount < CorrectionAmount then begin
CorrectionAmount := ForecastedRemainingAmount;
ForecastedRemainingAmount := 0;
end else
ForecastedRemainingAmount -= CorrectionAmount;
ConvertValueBasedOnType(CorrectionAmount, TimeSeriesForecast."Group ID");
// insert correction
CashFlowAzureAIBuffer.Init();
CashFlowAzureAIBuffer.Validate("Group Id", GroupIDValue);
CashFlowAzureAIBuffer.Validate(Amount, CorrectionAmount);
CashFlowAzureAIBuffer.Validate(Type, CashFlowAzureAIBuffer.Type::Correction);
CashFlowAzureAIBuffer.Validate("Period Start", TimeSeriesForecast."Period Start Date");
CashFlowAzureAIBuffer.Validate("Period Type", PeriodTypeValue);
CashFlowAzureAIBuffer.Validate("Period No.", TimeSeriesForecast."Period No.");
CashFlowAzureAIBuffer.Insert();
end;
local procedure GetCorrectionAmount(TimeSeriesForecast: Record "Time Series Forecast"; GroupIDValue: Text[100]; PeriodType: Option): Decimal
begin
case GroupIDValue of
XPAYABLESCORRECTIONTxt:
exit(CalculatePostedPurchDocsSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType));
XPURCHORDERSTxt:
exit(CalculateNotPostedPurchOrderSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType, false));
XRECEIVABLESCORRECTIONTxt:
exit(CalculatePostedSalesDocsSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType));
XSALESORDERSTxt:
exit(CalculateNotPostedSalesOrderSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType, false));
XTAXPAYABLESCORRECTIONTxt:
exit(CalculatePostedDocsSumTaxInPeriod(TimeSeriesForecast."Period Start Date", false));
XTAXPURCHORDERSTxt:
exit(CalculateNotPostedPurchOrderSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType, true));
XTAXRECEIVABLESCORRECTIONTxt:
exit(CalculatePostedDocsSumTaxInPeriod(TimeSeriesForecast."Period Start Date", true));
XTAXSALESORDERSTxt:
exit(CalculateNotPostedSalesOrderSumAmountInPeriod(TimeSeriesForecast."Period Start Date", PeriodType, true));
end;
end;
local procedure IsAmountValid(TimeSeriesForecast: Record "Time Series Forecast"): Boolean
begin
if ((TimeSeriesForecast."Group ID" = XPAYABLESTxt) or (TimeSeriesForecast."Group ID" = XTAXRECEIVABLESTxt)) and (TimeSeriesForecast.Value < 0) then
exit(true);
if ((TimeSeriesForecast."Group ID" = XRECEIVABLESTxt) or (TimeSeriesForecast."Group ID" = XTAXPAYABLESTxt)) and (TimeSeriesForecast.Value > 0) then
exit(true);
exit(false);
end;
local procedure GetForecastStartDate(PeriodType: Option Day,Week,Month,Quarter,Year): Date
begin
case PeriodType of
PeriodType::Day:
exit(WorkDate());
PeriodType::Week:
exit(CalcDate('<CW+1D-1W>', WorkDate()));
PeriodType::Month:
exit(CalcDate('<CM+1D-1M>', WorkDate()));
PeriodType::Quarter:
exit(CalcDate('<CQ+1D-1Q>', WorkDate()));
PeriodType::Year:
exit(CalcDate('<CY+1D-1Y>', WorkDate()));
end;
end;
local procedure CreateSetupNotification()
var
SetupNotification: Notification;
begin
if not ShowNotification() then
exit;
SetupNotification.Message := SetupScheduledForecastingMsg;
SetupNotification.Scope := NOTIFICATIONSCOPE::LocalScope;
SetupNotification.AddAction(EnableAzureAITxt, CODEUNIT::"Cash Flow Forecast Handler", 'SetupAzureAI');
SetupNotification.AddAction(DontAskAgainTxt, CODEUNIT::"Cash Flow Forecast Handler", 'DeactivateNotification');
SetupNotification.Send();
end;
local procedure ShowNotification(): Boolean
var
CashFlowSetup: Record "Cash Flow Setup";
O365GettingStarted: Record "O365 Getting Started";
begin
if O365GettingStarted.Get(UserId, ClientTypeManagement.GetCurrentClientType()) then
if O365GettingStarted."Tour in Progress" then
exit(false);
if not CashFlowSetup.Get() then
exit(false);
if CashFlowSetup."Azure AI Enabled" then
exit(false);
if not CashFlowSetup."Show AzureAI Notification" then
exit(false);
if CashFlowSetup."CF No. on Chart in Role Center" = '' then
exit(false);
exit(true);
end;
procedure DeactivateNotification(SetupNotification: Notification)
var
CashFlowSetup: Record "Cash Flow Setup";
begin
if not CashFlowSetup.Get() then
exit;
CashFlowSetup."Show AzureAI Notification" := false;
CashFlowSetup.Modify(true);
end;
procedure SetupAzureAI(SetupNotification: Notification)
var
EnvironmentInfo: Codeunit "Environment Information";
begin
if CashFlowSetup.Get() then
PAGE.RunModal(PAGE::"Cash Flow Setup", CashFlowSetup);
if CashFlowSetup.Get() then begin
if EnvironmentInfo.IsSaaS() and CashFlowSetup."Azure AI Enabled" then
Message(ScheduledForecastingEnabledMsg);
CashFlowSetup."Show AzureAI Notification" := false;
CashFlowSetup.Modify(true);
end;
end;
local procedure ConvertValueBasedOnType(var Value: Decimal; Type: Text)
begin
if (Type = XRECEIVABLESTxt) or (Type = XTAXPAYABLESTxt) then
Value := -Value;
end;
local procedure AggregateTaxRecordsToTaxablePeriod(var TimeSeriesForecast: Record "Time Series Forecast"; GroupID: Text[100])
var
NextForecastDate: Date;
TaxablePeriodEndDate: Date;
CurrentSum: Decimal;
LastPeriodNo: Integer;
PeriodTypeOption: Option Day,Week,Month,Quarter,Year;
begin
TimeSeriesForecast.SetRange("Group ID", GroupID);
CurrentSum := 0;
// Get the biggest taxable period endDate to avoid insertion of first record
if TimeSeriesForecast.FindLast() then
LastPeriodNo := TimeSeriesForecast."Period No.";
PeriodTypeOption := PeriodType;
if TimeSeriesForecast.FindSet() then
repeat
CurrentSum += TimeSeriesForecast.Value;
TaxablePeriodEndDate := CashFlowSetup.GetTaxPaymentDueDate(TimeSeriesForecast."Period Start Date");
TaxablePeriodEndDate := CashFlowSetup.GetTaxPeriodEndDate(TaxablePeriodEndDate);
NextForecastDate := TimeSeriesForecast."Period Start Date";
case PeriodType of
PeriodTypeOption::Day:
NextForecastDate := CalcDate('<+1D>', NextForecastDate);
PeriodTypeOption::Week:
NextForecastDate := CalcDate('<+1W>', NextForecastDate);
PeriodTypeOption::Month:
NextForecastDate := CalcDate('<+1M>', NextForecastDate);
PeriodTypeOption::Quarter:
NextForecastDate := CalcDate('<+1Q>', NextForecastDate);
PeriodTypeOption::Year:
NextForecastDate := CalcDate('<+1Y>', NextForecastDate);
end;
if (NextForecastDate > TaxablePeriodEndDate) or (TimeSeriesForecast."Period No." = LastPeriodNo) then begin
TimeSeriesForecast.Value := CurrentSum;
TimeSeriesForecast."Period Start Date" := CashFlowSetup.GetTaxPaymentDueDate(TimeSeriesForecast."Period Start Date");
TimeSeriesForecast.Modify();
CurrentSum := 0;
end else
TimeSeriesForecast.Delete();
until (TimeSeriesForecast.Next() = 0);
TimeSeriesForecast.SetRange("Group ID");
end;
[EventSubscriber(ObjectType::Page, Page::"Cash Flow Forecast Chart", 'OnOpenPageEvent', '', false, false)]
local procedure OnOpenCashFlowForecastChart(var Rec: Record "Business Chart Buffer")
begin
CreateSetupNotification();
end;
[IntegrationEvent(false, false)]
[Scope('OnPrem')]
procedure OnAfterPrepareSalesHistoryData(var TimeSeriesBuffer: Record "Time Series Buffer"; PeriodType: Integer; ForecastStartDate: Date; NumberOfPeriodsWithHistory: Integer)
begin
end;
[IntegrationEvent(false, false)]
[Scope('OnPrem')]
procedure OnAfterPreparePurchHistoryData(var TimeSeriesBuffer: Record "Time Series Buffer"; PeriodType: Integer; ForecastStartDate: Date; NumberOfPeriodsWithHistory: Integer)
begin
end;
[IntegrationEvent(false, false)]
[Scope('OnPrem')]
procedure OnAfterHasMinimumHistoricalData(var HasMinimumHistoryLoc: Boolean; var NumberOfPeriodsWithHistoryLoc: Integer; PeriodType: Integer; ForecastStartDate: Date)
begin
end;
}