Codeunit 1252 Match Bank Rec. Lines, source in 29
Source29
src/Layers/W1/BaseApp/Bank/Reconciliation/MatchBankRecLines.Codeunit.al1147 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.Bank.Reconciliation;
using Microsoft.Bank.BankAccount;
using Microsoft.Bank.Check;
using Microsoft.Bank.Ledger;
using Microsoft.Finance.GeneralLedger.Journal;
using System.Telemetry;
using System.Utilities;
/// <summary>
/// Provides automatic and manual matching capabilities for bank reconciliation statement lines.
/// This codeunit implements sophisticated algorithms to match imported bank statement lines with
/// existing bank account ledger entries for traditional bank reconciliation workflows. Supports
/// various matching criteria including amounts, dates, document numbers, and text descriptions,
/// with configurable tolerance settings and relationship mapping (one-to-one, one-to-many, many-to-one).
/// </summary>
/// <remarks>
/// Key features include automatic matching with scoring algorithms, manual matching override capabilities,
/// text similarity matching for complex scenarios, date tolerance handling, amount-based matching with
/// precision controls, and comprehensive reporting of match quality and confidence levels.
/// Integrates with bank account ledger entries, check ledger entries, and provides detailed telemetry
/// for performance monitoring and optimization. Used primarily for bank reconciliation rather than
/// payment application workflows, focusing on clearing outstanding transactions and identifying discrepancies.
/// </remarks>
codeunit 1252 "Match Bank Rec. Lines"
{
trigger OnRun()
begin
end;
var
#pragma warning disable AA0470
MatchSummaryMsg: Label '%1 reconciliation lines out of %2 are matched.\\';
#pragma warning restore AA0470
MatchDetailsTxt: Label 'This statement line matched the corresponding bank account ledger entry on the following fields: %1.', Comment = '%1 - a comma-separated list of field captions.';
MatchedManuallyTxt: Label 'This statement line was matched manually.';
#pragma warning disable AA0470
MissingMatchMsg: Label 'Text shorter than %1 characters cannot be matched.';
#pragma warning restore AA0470
ProgressBarMsgTemplateTok: Label '#1##############################', Locked = true;
ProgressBarUpdateTxt: Label 'Processed %1 statement lines.', Comment = '%1 - an integer';
ProgressBarMsg: Label 'Please wait while the operation is being completed.';
ManyToManyNotSupportedErr: Label 'Many-to-Many matchings are not supported';
OverwriteExistingMatchesTxt: Label 'There are lines in this statement that are already matched with ledger entries.\\ Do you want to overwrite the existing matches?';
NoMatchMsg: Label 'The selected lines have no matches to remove';
BankAccountRecTotalAndMatchedLinesLbl: Label 'Total Bank Statement Lines: %1, of those applied: %2', Locked = true;
AutomatchEventNameTelemetryTxt: Label 'Automatch', Locked = true;
BankAccountRecCategoryLbl: Label 'AL Bank Account Rec', Locked = true;
OptimizationDisabledLbl: Label 'Bank Rec. optimization disabled', Locked = true;
PotentiallyBetterMatchLbl: Label 'Bank Rec. better-candidate-found loop', Locked = true;
BankTransactionRequiresTextComparisonsLbl: Label 'Bank Ledger Entry requires text nearness with Bank Rec. Line', Locked = true;
BankRecLineHasNoDateLbl: Label 'Bank Rec. Line has no transaction date specified', Locked = true;
ToleranceDaysLbl: Label 'Tolerance window for matching', Locked = true;
EndOfClassicBankRecMatchLbl: Label 'End of classic Bank Rec. Automatch', Locked = true;
Relation: Option "One-to-One","One-to-Many","Many-to-One";
TextMatchGreater: Option First,Second,Tie;
MatchLengthTreshold: Integer;
NormalizingFactor: Integer;
/// <summary>
/// Creates manual matches between selected bank reconciliation lines and bank account ledger entries.
/// Enables users to override automatic matching decisions by explicitly linking statement lines
/// with specific ledger entries, supporting complex reconciliation scenarios requiring human judgment.
/// </summary>
/// <param name="SelectedBankAccReconciliationLine">Bank reconciliation lines selected for manual matching.</param>
/// <param name="SelectedBankAccountLedgerEntry">Bank account ledger entries to match with selected lines.</param>
procedure MatchManually(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var SelectedBankAccountLedgerEntry: Record "Bank Account Ledger Entry")
var
SelectedBankAccLECount: Integer;
SelectedBankAccRecLinesCount: Integer;
begin
SelectedBankAccLECount := SelectedBankAccountLedgerEntry.Count();
SelectedBankAccRecLinesCount := SelectedBankAccReconciliationLine.Count();
if SelectedBankAccLECount > 1 then
if SelectedBankAccRecLinesCount > 1 then
Error(ManyToManyNotSupportedErr);
if SelectedBankAccLECount >= SelectedBankAccRecLinesCount then
PerformOneToOneOrManyMatch(SelectedBankAccReconciliationLine, SelectedBankAccountLedgerEntry, Relation::"One-to-Many")
else
PerformManyToOneMatch(SelectedBankAccReconciliationLine, SelectedBankAccountLedgerEntry);
end;
local procedure PerformManyToOneMatch(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var SelectedBankAccountLedgerEntry: Record "Bank Account Ledger Entry")
var
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
PaymentMatchingDetails: Record "Payment Matching Details";
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
LineCount: Integer;
begin
if SelectedBankAccountLedgerEntry.FindFirst() then begin
BankAccountLedgerEntry.Get(SelectedBankAccountLedgerEntry."Entry No.");
BankAccEntrySetReconNo.RemoveApplication(BankAccountLedgerEntry);
LineCount := SelectedBankAccReconciliationLine.Count();
BankAccEntrySetReconNo.SetLineCount(LineCount);
RemoveMatchesFromRecLines(SelectedBankAccReconciliationLine);
if SelectedBankAccReconciliationLine.FindSet() then
repeat
BankAccReconciliationLine.GetBySystemId(SelectedBankAccReconciliationLine.SystemId);
OnPerformManyToOneMatchOnBeforeApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry);
BankAccEntrySetReconNo.ApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry, Relation::"Many-to-One");
OnPerformManyToOneMatchOnAfterApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry);
PaymentMatchingDetails.CreatePaymentMatchingDetail(BankAccReconciliationLine, MatchedManuallyTxt);
until SelectedBankAccReconciliationLine.Next() = 0;
end;
end;
local procedure PerformOneToOneOrManyMatch(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var SelectedBankAccountLedgerEntry: Record "Bank Account Ledger Entry"; Relation: Option "One-to-One","One-to-Many","Many-to-One")
var
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
PaymentMatchingDetails: Record "Payment Matching Details";
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
begin
if SelectedBankAccReconciliationLine.FindFirst() then begin
BankAccReconciliationLine.Get(
SelectedBankAccReconciliationLine."Statement Type",
SelectedBankAccReconciliationLine."Bank Account No.",
SelectedBankAccReconciliationLine."Statement No.",
SelectedBankAccReconciliationLine."Statement Line No.");
if BankReconciliationLineInManyToOne(SelectedBankAccReconciliationLine) then
RemoveMatchesFromRecLines(SelectedBankAccReconciliationLine);
if SelectedBankAccountLedgerEntry.FindSet() then
repeat
BankAccountLedgerEntry.Get(SelectedBankAccountLedgerEntry."Entry No.");
BankAccEntrySetReconNo.RemoveApplication(BankAccountLedgerEntry);
OnPerformOneToOneOrManyMatchOnBeforeApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry);
BankAccEntrySetReconNo.ApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry, Relation);
OnPerformOneToOneOrManyMatchOnAfterApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry);
PaymentMatchingDetails.CreatePaymentMatchingDetail(BankAccReconciliationLine, MatchedManuallyTxt);
until SelectedBankAccountLedgerEntry.Next() = 0;
end;
end;
/// <summary>
/// Determines whether a bank reconciliation line is part of a many-to-one matching relationship.
/// Checks if the line has multiple bank reconciliation lines matched to a single bank account
/// ledger entry, which requires special handling during removal and processing operations.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line to check for many-to-one relationship.</param>
/// <returns>True if the line is part of a many-to-one match; false otherwise.</returns>
internal procedure BankReconciliationLineInManyToOne(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"): Boolean
var
BankAccRecMatchBuffer: Record "Bank Acc. Rec. Match Buffer";
begin
BankAccReconciliationLine.FilterManyToOneMatches(BankAccRecMatchBuffer);
exit(not BankAccRecMatchBuffer.IsEmpty());
end;
/// <summary>
/// Removes all matches from selected bank reconciliation lines, clearing their applied entries.
/// Unlinks bank account ledger entries and check ledger entries from the reconciliation lines,
/// resetting them to unmatched status for potential re-matching with different entries.
/// </summary>
/// <param name="SelectedBankAccReconciliationLine">Bank reconciliation lines to clear matches from.</param>
procedure RemoveMatchesFromRecLines(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line")
var
MatchesRemoved: Boolean;
begin
RemoveMatchesFromRecLines(SelectedBankAccReconciliationLine, MatchesRemoved);
end;
/// <summary>
/// Removes all matches from selected bank reconciliation lines with detailed tracking of removal status.
/// Unlinks bank account ledger entries and check ledger entries from the reconciliation lines,
/// resetting them to unmatched status while providing feedback on whether any matches were actually removed.
/// </summary>
/// <param name="SelectedBankAccReconciliationLine">Bank reconciliation lines to clear matches from.</param>
/// <param name="MatchesRemoved">Returns true if any matches were removed; false if no matches existed.</param>
procedure RemoveMatchesFromRecLines(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var MatchesRemoved: Boolean)
var
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
CheckLedgerEntry: Record "Check Ledger Entry";
BankAccRecMatchBuffer: Record "Bank Acc. Rec. Match Buffer";
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
CheckEntrySetReconNo: Codeunit "Check Entry Set Recon.-No.";
IsHandled: Boolean;
begin
IsHandled := false;
OnBeforeRemoveMatchesFromRecLines(IsHandled);
if IsHandled then
exit;
if SelectedBankAccReconciliationLine.FindSet() then
repeat
BankAccReconciliationLine.Get(
SelectedBankAccReconciliationLine."Statement Type",
SelectedBankAccReconciliationLine."Bank Account No.",
SelectedBankAccReconciliationLine."Statement No.",
SelectedBankAccReconciliationLine."Statement Line No.");
BankAccountLedgerEntry.SetCurrentKey("Bank Account No.", Open);
BankAccountLedgerEntry.SetRange("Bank Account No.", BankAccReconciliationLine."Bank Account No.");
BankAccountLedgerEntry.SetRange("Statement No.", BankAccReconciliationLine."Statement No.");
BankAccountLedgerEntry.SetRange("Statement Line No.", BankAccReconciliationLine."Statement Line No.");
BankAccountLedgerEntry.SetRange(Open, true);
BankAccountLedgerEntry.SetRange("Statement Status", BankAccountLedgerEntry."Statement Status"::"Bank Acc. Entry Applied");
if BankAccountLedgerEntry.FindSet() then begin
MatchesRemoved := true;
repeat
BankAccEntrySetReconNo.RemoveApplication(BankAccountLedgerEntry);
until BankAccountLedgerEntry.Next() = 0;
end;
BankAccountLedgerEntry.Reset();
BankAccReconciliationLine.FilterManyToOneMatches(BankAccRecMatchBuffer);
if BankAccRecMatchBuffer.FindSet() then begin
MatchesRemoved := true;
repeat
BankAccountLedgerEntry.Get(BankAccRecMatchBuffer."Ledger Entry No.");
BankAccEntrySetReconNo.RemoveApplication(BankAccountLedgerEntry);
until BankAccRecMatchBuffer.Next() = 0;
end;
CheckLedgerEntry.SetCurrentKey("Bank Account No.", Open);
CheckLedgerEntry.SetRange("Bank Account No.", BankAccReconciliationLine."Bank Account No.");
CheckLedgerEntry.SetRange("Statement No.", BankAccReconciliationLine."Statement No.");
CheckLedgerEntry.SetRange("Statement Line No.", BankAccReconciliationLine."Statement Line No.");
CheckLedgerEntry.SetRange("Statement Status", CheckLedgerEntry."Statement Status"::"Check Entry Applied");
CheckLedgerEntry.SetRange(Open, true);
if CheckLedgerEntry.FindSet() then begin
MatchesRemoved := true;
repeat
CheckEntrySetReconNo.RemoveApplication(CheckLedgerEntry);
until CheckLedgerEntry.Next() = 0;
end;
until SelectedBankAccReconciliationLine.Next() = 0;
end;
/// <summary>
/// Removes specific matches between selected bank reconciliation lines and bank account ledger entries.
/// Provides granular control over match removal by targeting specific entry combinations rather than
/// clearing all matches from a line. Handles both bank account ledger entries and check ledger entries
/// with appropriate status updates and application removal.
/// </summary>
/// <param name="SelectedBankAccReconciliationLine">Bank reconciliation lines containing matches to remove.</param>
/// <param name="SelectedBankAccountLedgerEntry">Specific bank account ledger entries to unlink from the lines.</param>
procedure RemoveMatch(var SelectedBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var SelectedBankAccountLedgerEntry: Record "Bank Account Ledger Entry")
var
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
CheckLedgEntry: Record "Check Ledger Entry";
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
CheckEntrySetReconNo: Codeunit "Check Entry Set Recon.-No.";
MatchesRemoved: Boolean;
begin
MatchesRemoved := false;
RemoveMatchesFromRecLines(SelectedBankAccReconciliationLine, MatchesRemoved);
if SelectedBankAccountLedgerEntry.FindSet() then
repeat
case SelectedBankAccountLedgerEntry."Statement Status" of
SelectedBankAccountLedgerEntry."Statement Status"::"Bank Acc. Entry Applied":
begin
BankAccountLedgerEntry.Get(SelectedBankAccountLedgerEntry."Entry No.");
BankAccEntrySetReconNo.RemoveApplication(BankAccountLedgerEntry);
OnRemoveMatchOnAfterRemoveApplication(BankAccountLedgerEntry);
MatchesRemoved := true;
end;
SelectedBankAccountLedgerEntry."Statement Status"::"Check Entry Applied":
begin
CheckLedgEntry.Reset();
CheckLedgEntry.SetCurrentKey("Bank Account Ledger Entry No.");
CheckLedgEntry.SetRange("Bank Account Ledger Entry No.", SelectedBankAccountLedgerEntry."Entry No.");
CheckLedgEntry.SetRange(Open, true);
if CheckLedgEntry.FindSet() then
repeat
CheckEntrySetReconNo.RemoveApplication(CheckLedgEntry);
until CheckLedgEntry.Next() = 0;
MatchesRemoved := true;
end;
end;
until SelectedBankAccountLedgerEntry.Next() = 0;
if not MatchesRemoved then
Message(NoMatchMsg);
end;
/// <summary>
/// Executes automatic matching algorithm for bank account reconciliation with date tolerance.
/// Analyzes bank statement lines and bank account ledger entries to identify potential matches
/// based on amounts, dates (within tolerance), document numbers, and text descriptions.
/// Updates matched entries by applying them and setting statement references for reconciliation tracking.
/// </summary>
/// <param name="BankAccReconciliation">Bank account reconciliation to process for automatic matching.</param>
/// <param name="DaysTolerance">Number of days tolerance for date matching between statement and ledger entries.</param>
procedure BankAccReconciliationAutoMatch(BankAccReconciliation: Record "Bank Acc. Reconciliation"; DaysTolerance: Integer)
begin
BankAccReconciliationAutoMatch(BankAccReconciliation, DaysTolerance, false, true);
end;
/// <summary>
/// Executes comprehensive automatic matching algorithm for bank account reconciliation with advanced options.
/// Provides full control over the matching process including event handling, optimization settings,
/// and result presentation. Analyzes bank statement lines and bank account ledger entries using
/// sophisticated scoring algorithms based on amount, date, document number, and text similarity matching.
/// </summary>
/// <param name="BankAccReconciliation">Bank account reconciliation to process for automatic matching.</param>
/// <param name="DaysTolerance">Number of days tolerance for date matching between statement and ledger entries.</param>
/// <param name="RaiseFindBestMatchesEvent">Whether to raise integration events for custom matching logic.</param>
/// <param name="ShouldShowMatchSummary">Whether to display summary notification with matching results.</param>
procedure BankAccReconciliationAutoMatch(var BankAccReconciliation: Record "Bank Acc. Reconciliation"; DaysTolerance: Integer; RaiseFindBestMatchesEvent: Boolean; ShouldShowMatchSummary: Boolean)
var
BankAccount: Record "Bank Account";
TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary;
TempBankAccLedgerEntryMatchingBuffer: Record "Ledger Entry Matching Buffer" temporary;
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
UndoMatchOnBankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
ConfirmManagement: Codeunit "Confirm Management";
FeatureTelemetry: Codeunit "Feature Telemetry";
ProgressDialog: Dialog;
Overwrite: Boolean;
DisableOptimization: Boolean;
RemovedPreviouslyAssigned: Boolean;
Handled: Boolean;
BankAccRecLineCounter: Integer;
OriginallyMatchedBankAccRecLineNos: List of [Integer];
begin
BankAccount.Get(BankAccReconciliation."Bank Account No.");
DisableOptimization := BankAccount."Disable Bank Rec. Optimization";
if DisableOptimization then
Session.LogMessage('0000NUK', OptimizationDisabledLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
Session.LogMessage('0000NUL', ToleranceDaysLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl, 'Tolerance', Format(DaysTolerance));
FeatureTelemetry.LogUptake('0000JLB', BankAccReconciliation.GetBankReconciliationTelemetryFeatureName(), Enum::"Feature Uptake Status"::Used);
Overwrite := true;
BankAccRecLineCounter := 0;
BankAccReconciliationLine.FilterBankRecLines(BankAccReconciliation);
BankAccReconciliationLine.SetFilter("Applied Entries", '<>%1', 0);
if not BankAccReconciliationLine.IsEmpty() then begin
Overwrite := ConfirmManagement.GetResponseOrDefault(OverwriteExistingMatchesTxt, false);
if Overwrite then begin
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.SetFilter("Bank Account No.", BankAccReconciliation."Bank Account No.");
BankAccReconciliationLine.SetFilter("Statement No.", BankAccReconciliation."Statement No.");
RemoveMatchesFromRecLines(BankAccReconciliationLine);
end else
if BankAccReconciliationLine.FindSet() then
repeat
OriginallyMatchedBankAccRecLineNos.Add(BankAccReconciliationLine."Statement Line No.");
until BankAccReconciliationLine.Next() = 0;
end;
if GuiAllowed() then begin
ProgressDialog.Open(ProgressBarMsgTemplateTok);
ProgressDialog.Update(1, ProgressBarMsg);
end;
// Lines to match
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.FilterBankRecLinesByDate(BankAccReconciliation, Overwrite);
// Candidate Bank Account Ledger Entries
BankAccountLedgerEntry.SetBankReconciliationCandidatesFilter(BankAccReconciliation);
InitializeBLEMatchingTempTable(TempBankAccLedgerEntryMatchingBuffer, BankAccountLedgerEntry);
// Both candidate Bank Account Ledger Entries and lines to match are sorted by dates as this can be used to
// easily get rid of many matching candidates outside the range.
// Bank Rec. Lines may have an empty Transaction date, they are sorted first and these will
// be attempted to match with every other candidate.
repeat
TempBankStatementMatchingBuffer.DeleteAll();
RemovedPreviouslyAssigned := false;
if not TempBankAccLedgerEntryMatchingBuffer.IsEmpty() then
if BankAccReconciliationLine.FindSet() then
repeat
if GuiAllowed() then
if BankAccRecLineCounter > 999 then
if BankAccRecLineCounter mod 1000 = 0 then
ProgressDialog.Update(1, StrSubstNo(ProgressBarUpdateTxt, BankAccRecLineCounter));
// If there are no candidate Bank Account Ledger Entries left, we can stop as there will be no other match
if TempBankAccLedgerEntryMatchingBuffer.IsEmpty() then
break;
AttemptToMatch(BankAccReconciliationLine, TempBankAccLedgerEntryMatchingBuffer, DaysTolerance, TempBankStatementMatchingBuffer, RemovedPreviouslyAssigned, DisableOptimization);
BankAccRecLineCounter += 1
until (BankAccReconciliationLine.Next() = 0);
SaveOneToOneMatching(TempBankStatementMatchingBuffer, BankAccReconciliation."Bank Account No.", BankAccReconciliation."Statement No.");
if RemovedPreviouslyAssigned then begin
Session.LogMessage('0000NUM', PotentiallyBetterMatchLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
BankAccountLedgerEntry.SetBankReconciliationCandidatesFilter(BankAccReconciliation);
InitializeBLEMatchingTempTable(TempBankAccLedgerEntryMatchingBuffer, BankAccountLedgerEntry);
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.FilterBankRecLinesByDate(BankAccReconciliation, false);
end;
until not RemovedPreviouslyAssigned;
Session.LogMessage('0000NUN', EndOfClassicBankRecMatchLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
if GuiAllowed() then
ProgressDialog.Close();
if RaiseFindBestMatchesEvent then begin
BankAccountLedgerEntry.SetBankReconciliationCandidatesFilter(BankAccReconciliation);
InitializeBLEMatchingTempTable(TempBankAccLedgerEntryMatchingBuffer, BankAccountLedgerEntry);
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.FilterBankRecLinesByDate(BankAccReconciliation, false);
if BankAccReconciliationLine.FindSet() then begin
OnFindBestMatches(BankAccReconciliationLine, TempBankAccLedgerEntryMatchingBuffer, DaysTolerance, TempBankStatementMatchingBuffer, RemovedPreviouslyAssigned, Handled);
if Handled then begin
BankAccountLedgerEntry.SetBankReconciliationCandidatesFilter(BankAccReconciliation);
InitializeBLEMatchingTempTable(TempBankAccLedgerEntryMatchingBuffer, BankAccountLedgerEntry);
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.FilterBankRecLinesByDate(BankAccReconciliation, false);
end else begin
UndoMatchOnBankAccReconciliationLine.FilterBankRecLines(BankAccReconciliation);
UndoMatchOnBankAccReconciliationLine.SetFilter("Applied Entries", '<>%1', 0);
if UndoMatchOnBankAccReconciliationLine.FindSet() then
repeat
if not OriginallyMatchedBankAccRecLineNos.Contains(UndoMatchOnBankAccReconciliationLine."Statement Line No.") then
UndoMatchOnBankAccReconciliationLine.Mark(true);
until UndoMatchOnBankAccReconciliationLine.Next() = 0;
UndoMatchOnBankAccReconciliationLine.MarkedOnly(true);
RemoveMatchesFromRecLines(UndoMatchOnBankAccReconciliationLine);
end;
end;
end;
if ShouldShowMatchSummary then
ShowMatchSummary(BankAccReconciliation);
FeatureTelemetry.LogUsage('0000JLC', BankAccReconciliation.GetBankReconciliationTelemetryFeatureName(), AutomatchEventNameTelemetryTxt);
end;
/// <summary>
/// Given a BankAccReconciliationLine it attempts to find a match from the non-empty temporary table candidates in
/// TempBankAccLedgerEntryMatchingBuffer. It considers DaysTolerance as configuration parameter.
///
/// It uses the assumption that entries in TempBankAccLedgerEntryMatchingBuffer are sorted ascendingly by Posting Date and that
/// subsequent calls during an automatch have their Transaction Date increasing as well.
///
/// If a match is found, the match details are inserted in the TempBankStatementMatchingBuffer. Multiple matches can be inserted.
///
/// </summary>
/// <param name="BankAccReconciliationLine">Record loaded with the line to find a match for, subsequent calls make the assumption that it was called on a set ordered by Transaction Date</param>
/// <param name="TempBankAccLedgerEntryMatchingBuffer">Candidate Bank Account Ledger Entries to consider for the match sorted by Posting Date</param>
/// <param name="DaysTolerance">Days of tolerance allowed</param>
/// <param name="TempBankStatementMatchingBuffer">Temporary table where the match is inserted if found</param>
local procedure AttemptToMatch(BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var TempBankAccLedgerEntryMatchingBuffer: Record "Ledger Entry Matching Buffer" temporary; DaysTolerance: Integer; var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var RemovedPreviouslyAssigned: Boolean; DisableOptimization: Boolean)
var
TempBestBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary;
TempMatchingDetailsBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary;
BankTransactionTooEarly: Boolean;
BankTransactionTooLate: Boolean;
ShouldContinueTryingToMatch: Boolean;
begin
TempBestBankStatementMatchingBuffer.Reset();
if not TempBankAccLedgerEntryMatchingBuffer.FindSet() then
exit;
repeat
// Date assumptions are only considered when Bank Rec. Line has Transaction Date <> 0
if BankAccReconciliationLine."Transaction Date" <> 0D then begin
BankTransactionTooEarly := (BankAccReconciliationLine."Transaction Date" - TempBankAccLedgerEntryMatchingBuffer."Posting Date") > DaysTolerance;
BankTransactionTooLate := (TempBankAccLedgerEntryMatchingBuffer."Posting Date" - BankAccReconciliationLine."Transaction Date") > DaysTolerance;
end
else
Session.LogMessage('0000NUO', BankRecLineHasNoDateLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
// Subsequent calls to this method are with Bank Rec Lines sorted ascending by Transaction date, therefore
// if a candidate BLE is too early for this Bank Rec Line, it will be too early for posterior calls as well.
// These are removed from the candidates for optimization.
if BankTransactionTooEarly then
TempBankAccLedgerEntryMatchingBuffer.Delete();
if (not BankTransactionTooEarly) and (not BankTransactionTooLate) then begin
TempMatchingDetailsBankStatementMatchingBuffer.Reset();
Session.LogMessage('0000NUP', BankTransactionRequiresTextComparisonsLbl, Verbosity::Verbose, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
if MatchingIsAcceptable(BankAccReconciliationLine, TempBankAccLedgerEntryMatchingBuffer, TempMatchingDetailsBankStatementMatchingBuffer) then begin
if (not DisableOptimization) and MatchingIsHighConfidence(TempMatchingDetailsBankStatementMatchingBuffer) then
if AddMatchToCandidatesIfBetter(TempBankStatementMatchingBuffer, TempMatchingDetailsBankStatementMatchingBuffer, RemovedPreviouslyAssigned) then
exit;
if MatchIsBetter(TempMatchingDetailsBankStatementMatchingBuffer, TempBestBankStatementMatchingBuffer) then
if BestMatchingCandidateForBankAccountLedgerEntryIsWorse(TempBankStatementMatchingBuffer, TempMatchingDetailsBankStatementMatchingBuffer) then
TempBestBankStatementMatchingBuffer.Copy(TempMatchingDetailsBankStatementMatchingBuffer);
end
end;
// If the candidate is too late, then we can stop already as Bank Ledger Entries are sorted by Posting Date (BankTransactionTooLate will always be true after)
ShouldContinueTryingToMatch := (TempBankAccLedgerEntryMatchingBuffer.Next() <> 0) and (not BankTransactionTooLate);
until not ShouldContinueTryingToMatch;
if TempBestBankStatementMatchingBuffer."Entry No." <> 0 then
AddMatchToCandidatesIfBetter(TempBankStatementMatchingBuffer, TempBestBankStatementMatchingBuffer, RemovedPreviouslyAssigned);
end;
local procedure AddMatchToCandidatesIfBetter(var CandidatesBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var ToAddBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var RemovedPreviouslyAssigned: Boolean): Boolean
begin
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.", ToAddBankStatementMatchingBuffer."Entry No.");
if CandidatesBankStatementMatchingBuffer.IsEmpty() then begin
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.");
AddMatchingCandidate(CandidatesBankStatementMatchingBuffer, ToAddBankStatementMatchingBuffer);
exit(true);
end;
if BestMatchingCandidateForBankAccountLedgerEntryIsWorse(CandidatesBankStatementMatchingBuffer, ToAddBankStatementMatchingBuffer) then begin
RemovedPreviouslyAssigned := true;
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.");
RemoveBestMatchingCandidateForBankAccountLedgerEntry(CandidatesBankStatementMatchingBuffer, ToAddBankStatementMatchingBuffer."Entry No.");
AddMatchingCandidate(CandidatesBankStatementMatchingBuffer, ToAddBankStatementMatchingBuffer);
exit(true);
end;
exit(false)
end;
local procedure RemoveBestMatchingCandidateForBankAccountLedgerEntry(var CandidatesBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; BankAccountLedgerEntryNo: Integer)
begin
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.", BankAccountLedgerEntryNo);
CandidatesBankStatementMatchingBuffer.FindFirst();
CandidatesBankStatementMatchingBuffer.Delete();
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.");
end;
local procedure BestMatchingCandidateForBankAccountLedgerEntryIsWorse(var CandidatesBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var ToAddBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Boolean
var
Result: Boolean;
begin
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.", ToAddBankStatementMatchingBuffer."Entry No.");
if CandidatesBankStatementMatchingBuffer.IsEmpty() then begin
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.");
exit(true);
end;
CandidatesBankStatementMatchingBuffer.FindFirst();
Result := MatchIsBetter(ToAddBankStatementMatchingBuffer, CandidatesBankStatementMatchingBuffer);
CandidatesBankStatementMatchingBuffer.SetRange("Entry No.");
exit(Result);
end;
local procedure AddMatchingCandidate(var CandidatesBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var ToAddBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary)
begin
CandidatesBankStatementMatchingBuffer.Copy(ToAddBankStatementMatchingBuffer);
CandidatesBankStatementMatchingBuffer.Insert();
end;
/// <summary>
/// Returns true if TempBankStatementMatchingBuffer is better than TempToCompareBankStatementMatchingBuffer
/// </summary>
/// <param name="TempBankStatementMatchingBuffer"></param>
/// <param name="TempToCompareBankStatementMatchingBuffer"></param>
/// <returns></returns>
local procedure MatchIsBetter(var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var TempToCompareBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Boolean
var
AmountDifferenceSmaller: Boolean;
DateDifferenceSmaller: Boolean;
TextMatchingBetter: Boolean;
begin
if TempToCompareBankStatementMatchingBuffer."Entry No." = 0 then
exit(true);
AmountDifferenceSmaller := Abs(TempBankStatementMatchingBuffer."Amount Difference") < Abs(TempToCompareBankStatementMatchingBuffer."Amount Difference");
DateDifferenceSmaller := Abs(TempBankStatementMatchingBuffer."Date Difference") < Abs(TempToCompareBankStatementMatchingBuffer."Date Difference");
TextMatchingBetter := IsTextMatchingBetter(TempBankStatementMatchingBuffer, TempToCompareBankStatementMatchingBuffer);
if AmountDifferenceSmaller then
exit(true);
if Abs(TempBankStatementMatchingBuffer."Amount Difference") > Abs(TempToCompareBankStatementMatchingBuffer."Amount Difference") then
exit(false);
// Amount differences are equal in both matches
if TextMatchingBetter then
exit(true);
if IsTextMatchingBetter(TempToCompareBankStatementMatchingBuffer, TempBankStatementMatchingBuffer) then
exit(false);
// Text scores are equally good in both matches
exit(DateDifferenceSmaller);
end;
/// <summary>
/// Returns true if the text matchings in TempBankStatementMatchingBuffer are better than TempToCompareBankStatementMatchingBuffer
/// </summary>
/// <param name="TempBankStatementMatchingBuffer"></param>
/// <param name="TempToCompareBankStatementMatchingBuffer"></param>
/// <returns></returns>
local procedure IsTextMatchingBetter(var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var TempToCompareBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Boolean
var
FirstDocNoScore, SecondDocNoScore, WinnerDocNoScore : Integer;
FirstExtDocNoScore, SecondExtDocNoScore, WinnerExtDocNoScore : Integer;
FirstDescriptionScore, SecondDescriptionScore, WinnerDescriptionScore : Integer;
ComparisonResultDocNo, ComparisonResultExtDocNo, ComparisonResultDescription : Option;
BestMaxWinner: Option;
begin
FirstDocNoScore := GetMax(TempBankStatementMatchingBuffer."Doc. No. Score", TempBankStatementMatchingBuffer."Doc. No. Exact Score");
SecondDocNoScore := GetMax(TempToCompareBankStatementMatchingBuffer."Doc. No. Score", TempToCompareBankStatementMatchingBuffer."Doc. No. Exact Score");
FirstExtDocNoScore := GetMax(TempBankStatementMatchingBuffer."Ext. Doc. No. Score", TempBankStatementMatchingBuffer."Ext. Doc. No. Exact Score");
SecondExtDocNoScore := GetMax(TempToCompareBankStatementMatchingBuffer."Ext. Doc. No. Score", TempToCompareBankStatementMatchingBuffer."Ext. Doc. No. Exact Score");
FirstDescriptionScore := GetMax(TempBankStatementMatchingBuffer."Description Score", TempBankStatementMatchingBuffer."Description Exact Score");
SecondDescriptionScore := GetMax(TempToCompareBankStatementMatchingBuffer."Description Score", TempToCompareBankStatementMatchingBuffer."Description Exact Score");
TextScoreGreaterThan(FirstDocNoScore, SecondDocNoScore, ComparisonResultDocNo, WinnerDocNoScore);
TextScoreGreaterThan(FirstExtDocNoScore, SecondExtDocNoScore, ComparisonResultExtDocNo, WinnerExtDocNoScore);
TextScoreGreaterThan(FirstDescriptionScore, SecondDescriptionScore, ComparisonResultDescription, WinnerDescriptionScore);
GetMaxScoreOfWinner(ComparisonResultDocNo, WinnerDocNoScore, ComparisonResultExtDocNo, WinnerExtDocNoScore, ComparisonResultDescription, WinnerDescriptionScore, BestMaxWinner);
if BestMaxWinner = TextMatchGreater::Tie then
exit(false);
if BestMaxWinner = TextMatchGreater::First then
exit(true);
if BestMaxWinner = TextMatchGreater::Second then
exit(false);
end;
/// <summary>
/// There are 3 scores associated to how well does Bank Rec. Line **text fields** match a specific Bank Account Ledger Entry:
/// - Doc. No Score
/// - Ext. Doc. No Score
/// - Description Score
/// When we compare two matches, we first compare how well they do on each category. If one match is better in a category, it wins that category.
/// The Option parameter values are used to store the winner of each category. The Integer parameter values contain the winning score for that category.
///
/// The best match is considered as the one for which a category has the highest winning score. This reflects the fact that there is usually one field used to match exactly, while the other fields do not necessarily matter.
/// The result of the best match is stored in the output parameter BestMaxWinner.
/// </summary>
local procedure GetMaxScoreOfWinner(ComparisonResultDocNo: Option; WinnerDocNoScore: Integer; ComparisonResultExtDocNo: Option; WinnerExtDocNoScore: Integer; ComparisonResultDescription: Option; WinnerDescriptionScore: Integer; var BestMaxWinner: Option)
var
MaxWinningScore: Integer;
begin
BestMaxWinner := TextMatchGreater::Tie;
if ComparisonResultDocNo <> TextMatchGreater::Tie then begin
MaxWinningScore := WinnerDocNoScore;
BestMaxWinner := ComparisonResultDocNo
end;
if BestMaxWinner = TextMatchGreater::Tie then begin
if ComparisonResultExtDocNo <> TextMatchGreater::Tie then begin
MaxWinningScore := WinnerExtDocNoScore;
BestMaxWinner := ComparisonResultExtDocNo;
end;
end else
if ComparisonResultExtDocNo <> TextMatchGreater::Tie then
if MaxWinningScore < WinnerExtDocNoScore then begin
MaxWinningScore := WinnerExtDocNoScore;
BestMaxWinner := ComparisonResultExtDocNo;
end;
if BestMaxWinner = TextMatchGreater::Tie then begin
if ComparisonResultDescription <> TextMatchGreater::Tie then begin
MaxWinningScore := WinnerDescriptionScore;
BestMaxWinner := ComparisonResultDescription;
end;
end else
if ComparisonResultDescription <> TextMatchGreater::Tie then
if MaxWinningScore < WinnerDescriptionScore then begin
MaxWinningScore := WinnerDescriptionScore;
BestMaxWinner := ComparisonResultDescription;
end;
end;
local procedure TextScoreGreaterThan(TextScore1: Integer; TextScore2: Integer; var Result: Option; var WinnerScoreValue: Integer)
begin
if TextScore1 > TextScore2 then begin
WinnerScoreValue := TextScore1;
Result := TextMatchGreater::First;
exit;
end;
if TextScore2 > TextScore1 then begin
WinnerScoreValue := TextScore2;
Result := TextMatchGreater::Second;
exit;
end;
WinnerScoreValue := TextScore1;
Result := TextMatchGreater::Tie;
end;
local procedure GetMax(N1: Integer; N2: Integer): Integer
begin
if N1 > N2 then
exit(N1);
exit(N2);
end;
local procedure GetMaxTextNearnessScore(TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Integer
var
MaxTextScore: Integer;
begin
MaxTextScore := TempBankStatementMatchingBuffer."Doc. No. Score";
if MaxTextScore < TempBankStatementMatchingBuffer."Ext. Doc. No. Score" then
MaxTextScore := TempBankStatementMatchingBuffer."Ext. Doc. No. Score";
if MaxTextScore < TempBankStatementMatchingBuffer."Description Score" then
MaxTextScore := TempBankStatementMatchingBuffer."Description Score";
exit(MaxTextScore);
end;
local procedure GetMaxTextExactScore(TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Integer
var
MaxTextScore: Integer;
begin
MaxTextScore := TempBankStatementMatchingBuffer."Doc. No. Exact Score";
if MaxTextScore < TempBankStatementMatchingBuffer."Ext. Doc. No. Exact Score" then
MaxTextScore := TempBankStatementMatchingBuffer."Ext. Doc. No. Exact Score";
if MaxTextScore < TempBankStatementMatchingBuffer."Description Exact Score" then
MaxTextScore := TempBankStatementMatchingBuffer."Description Exact Score";
exit(MaxTextScore);
end;
local procedure MatchingIsHighConfidence(TempMatchingDetailsBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Boolean
begin
exit((TempMatchingDetailsBankStatementMatchingBuffer."Date Difference" = 0) and
(TempMatchingDetailsBankStatementMatchingBuffer."Amount Difference" = 0) and
(GetMaxTextNearnessScore(TempMatchingDetailsBankStatementMatchingBuffer) >= 95) and
(GetMaxTextExactScore(TempMatchingDetailsBankStatementMatchingBuffer) >= 95));
end;
local procedure MatchingIsAcceptable(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var TempBankAccLedgerEntryMatchingBuffer: Record "Ledger Entry Matching Buffer" temporary; var TempMatchingDetailsBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary): Boolean
var
AmountMatched: Boolean;
AmountClose: Boolean;
TransactionDateMatched: Boolean;
DocumentNoMatched: Boolean;
ExternalDocumentNoMatched: Boolean;
DescriptionMatched: Boolean;
ListOfMatchFields: Text;
begin
// We compute and load all the required information about their match
TempMatchingDetailsBankStatementMatchingBuffer.Init();
if BankAccReconciliationLine."Transaction Date" = 0D then
TempMatchingDetailsBankStatementMatchingBuffer."Date Difference" := 0
else
TempMatchingDetailsBankStatementMatchingBuffer."Date Difference" := TempBankAccLedgerEntryMatchingBuffer."Posting Date" - BankAccReconciliationLine."Transaction Date";
TempMatchingDetailsBankStatementMatchingBuffer."Amount Difference" := TempBankAccLedgerEntryMatchingBuffer."Remaining Amount" - BankAccReconciliationLine.Difference;
AmountMatched := (TempMatchingDetailsBankStatementMatchingBuffer."Amount Difference" = 0);
AmountClose := IsAmountClose(TempBankAccLedgerEntryMatchingBuffer."Remaining Amount", BankAccReconciliationLine.Difference);
if not AmountClose then
exit(false);
BankReconciliationLineTextSimilarityScores(TempBankAccLedgerEntryMatchingBuffer."Document No.", BankAccReconciliationLine, TempMatchingDetailsBankStatementMatchingBuffer."Doc. No. Score", TempMatchingDetailsBankStatementMatchingBuffer."Doc. No. Exact Score");
BankReconciliationLineTextSimilarityScores(TempBankAccLedgerEntryMatchingBuffer."External Document No.", BankAccReconciliationLine, TempMatchingDetailsBankStatementMatchingBuffer."Ext. Doc. No. Score", TempMatchingDetailsBankStatementMatchingBuffer."Ext. Doc. No. Exact Score");
BankReconciliationLineTextSimilarityScores(TempBankAccLedgerEntryMatchingBuffer.Description, BankAccReconciliationLine, TempMatchingDetailsBankStatementMatchingBuffer."Description Score", TempMatchingDetailsBankStatementMatchingBuffer."Description Exact Score");
TempMatchingDetailsBankStatementMatchingBuffer."Line No." := BankAccReconciliationLine."Statement Line No.";
TempMatchingDetailsBankStatementMatchingBuffer."Entry No." := TempBankAccLedgerEntryMatchingBuffer."Entry No.";
TempMatchingDetailsBankStatementMatchingBuffer."Account Type" := Enum::"Gen. Journal Account Type"::"G/L Account";
TempMatchingDetailsBankStatementMatchingBuffer."Account No." := '';
if BankAccReconciliationLine."Transaction Date" <> 0D then
TransactionDateMatched := (TempMatchingDetailsBankStatementMatchingBuffer."Date Difference" = 0);
DocumentNoMatched := (TempMatchingDetailsBankStatementMatchingBuffer."Doc. No. Score" >= 80) or (TempMatchingDetailsBankStatementMatchingBuffer."Doc. No. Exact Score" = 100);
ExternalDocumentNoMatched := (TempMatchingDetailsBankStatementMatchingBuffer."Ext. Doc. No. Score" >= 80) or (TempMatchingDetailsBankStatementMatchingBuffer."Ext. Doc. No. Exact Score" = 100);
DescriptionMatched := (TempMatchingDetailsBankStatementMatchingBuffer."Description Score" >= 80) or (TempMatchingDetailsBankStatementMatchingBuffer."Description Exact Score" = 100);
ListOfMatchFields := ListOfMatchedFields(AmountMatched, DocumentNoMatched, ExternalDocumentNoMatched, TransactionDateMatched, false, DescriptionMatched);
TempMatchingDetailsBankStatementMatchingBuffer."Match Details" := StrSubstNo(MatchDetailsTxt, ListOfMatchFields);
if AmountMatched then
exit(true);
exit(DocumentNoMatched or ExternalDocumentNoMatched or DescriptionMatched);
end;
local procedure IsAmountClose(TargetAmount: Decimal; StatementAmount: Decimal): Boolean
var
Ratio: Decimal;
begin
if (TargetAmount = 0) or (StatementAmount = 0) then
exit(false);
if not SameSign(TargetAmount, StatementAmount) then
exit(false);
Ratio := Abs(StatementAmount) / Abs(TargetAmount);
exit((Ratio > 0.6) and (Ratio < 1.1));
end;
local procedure SameSign(Amount1: Decimal; Amount2: Decimal): Boolean
begin
if (Amount1 = 0) or (Amount2 = 0) then
exit(false);
exit((Amount1 div Abs(Amount1)) = (Amount2 div Abs(Amount2)));
end;
/// <summary>
/// Computes both nearness and exact scores between TextToMatch and all BankAccReconciliationLine text fields in a
/// single pass, short-circuiting as soon as both scores reach 100.
/// Fields checked in order: Description, Related-Party Name, Additional Transaction Info, Document No.
/// </summary>
/// <param name="TextToMatch">The text from the Bank Account Ledger Entry to match against</param>
/// <param name="BankAccReconciliationLine">The bank reconciliation line whose fields are compared</param>
/// <param name="NearScore">Output: best nearness score (0-100) across all line fields</param>
/// <param name="ExactScore">Output: best exact score (0-100) across all line fields</param>
local procedure BankReconciliationLineTextSimilarityScores(TextToMatch: Text; BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var NearScore: Integer; var ExactScore: Integer)
var
RecordMatchMgt: Codeunit "Record Match Mgt.";
FieldNear: Integer;
FieldExact: Integer;
begin
NearScore := 0;
ExactScore := 0;
if TextToMatch = '' then
exit;
FieldNear := RecordMatchMgt.CalculateStringNearness(BankAccReconciliationLine.Description, TextToMatch, 4, 100);
FieldExact := RecordMatchMgt.CalculateExactStringNearness(BankAccReconciliationLine.Description, TextToMatch, 100);
if FieldNear > NearScore then
NearScore := FieldNear;
if FieldExact > ExactScore then
ExactScore := FieldExact;
if (NearScore = 100) and (ExactScore = 100) then
exit;
FieldNear := RecordMatchMgt.CalculateStringNearness(BankAccReconciliationLine."Related-Party Name", TextToMatch, 4, 100);
FieldExact := RecordMatchMgt.CalculateExactStringNearness(BankAccReconciliationLine."Related-Party Name", TextToMatch, 100);
if FieldNear > NearScore then
NearScore := FieldNear;
if FieldExact > ExactScore then
ExactScore := FieldExact;
if (NearScore = 100) and (ExactScore = 100) then
exit;
FieldNear := RecordMatchMgt.CalculateStringNearness(BankAccReconciliationLine."Additional Transaction Info", TextToMatch, 4, 100);
FieldExact := RecordMatchMgt.CalculateExactStringNearness(BankAccReconciliationLine."Additional Transaction Info", TextToMatch, 100);
if FieldNear > NearScore then
NearScore := FieldNear;
if FieldExact > ExactScore then
ExactScore := FieldExact;
if (NearScore = 100) and (ExactScore = 100) then
exit;
FieldNear := RecordMatchMgt.CalculateStringNearness(BankAccReconciliationLine."Document No.", TextToMatch, 4, 100);
FieldExact := RecordMatchMgt.CalculateExactStringNearness(BankAccReconciliationLine."Document No.", TextToMatch, 100);
if FieldNear > NearScore then
NearScore := FieldNear;
if FieldExact > ExactScore then
ExactScore := FieldExact;
end;
procedure InitializeBLEMatchingTempTable(var TempBankAccLedgerEntryMatchingBuffer: Record "Ledger Entry Matching Buffer" temporary; var BankAccountLedgerEntry: Record "Bank Account Ledger Entry")
begin
TempBankAccLedgerEntryMatchingBuffer.Reset();
TempBankAccLedgerEntryMatchingBuffer.DeleteAll();
if not BankAccountLedgerEntry.FindSet() then
exit;
repeat
TempBankAccLedgerEntryMatchingBuffer.InsertFromBankAccLedgerEntry(BankAccountLedgerEntry);
until BankAccountLedgerEntry.Next() = 0;
TempBankAccLedgerEntryMatchingBuffer.SetCurrentKey("Posting Date");
TempBankAccLedgerEntryMatchingBuffer.SetAscending("Posting Date", true);
end;
procedure ListOfMatchedFields(AmountMatched: Boolean; DocumentNoMatched: Boolean; ExternalDocumentNoMatched: Boolean; TransactionDateMatched: Boolean; RelatedPartyMatched: Boolean; DescriptionMatched: Boolean): Text
var
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
ListOfFields: Text;
Comma: Text;
begin
Comma := ', ';
if AmountMatched then
ListOfFields += BankAccountLedgerEntry.FieldCaption(BankAccountLedgerEntry."Remaining Amount");
if DocumentNoMatched then begin
if ListOfFields <> '' then
ListOfFields += Comma;
ListOfFields += BankAccountLedgerEntry.FieldCaption(BankAccountLedgerEntry."Document No.");
end;
if ExternalDocumentNoMatched then begin
if ListOfFields <> '' then
ListOfFields += Comma;
ListOfFields += BankAccountLedgerEntry.FieldCaption(BankAccountLedgerEntry."External Document No.");
end;
if TransactionDateMatched then begin
if ListOfFields <> '' then
ListOfFields += Comma;
ListOfFields += BankAccountLedgerEntry.FieldCaption(BankAccountLedgerEntry."Posting Date");
end;
if RelatedPartyMatched or DescriptionMatched then begin
if ListOfFields <> '' then
ListOfFields += Comma;
ListOfFields += BankAccountLedgerEntry.FieldCaption(BankAccountLedgerEntry.Description);
end;
exit(ListOfFields);
end;
procedure SaveOneToOneMatching(var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; BankAccountNo: Code[20]; StatementNo: Code[20])
var
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
PaymentMatchingDetails: Record "Payment Matching Details";
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
begin
// seems to be only used by auto match
TempBankStatementMatchingBuffer.Reset();
TempBankStatementMatchingBuffer.SetCurrentKey(Quality);
TempBankStatementMatchingBuffer.Ascending(false);
if TempBankStatementMatchingBuffer.FindSet() then
repeat
BankAccountLedgerEntry.Get(TempBankStatementMatchingBuffer."Entry No.");
BankAccReconciliationLine.Get(
BankAccReconciliationLine."Statement Type"::"Bank Reconciliation",
BankAccountNo, StatementNo,
TempBankStatementMatchingBuffer."Line No.");
if BankAccEntrySetReconNo.ApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry, Relation::"One-to-Many") then
PaymentMatchingDetails.CreatePaymentMatchingDetail(BankAccReconciliationLine, TempBankStatementMatchingBuffer."Match Details");
until TempBankStatementMatchingBuffer.Next() = 0;
end;
procedure SaveManyToOneMatching(var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; BankAccountNo: Code[20]; StatementNo: Code[20])
var
BankAccountLedgerEntry: Record "Bank Account Ledger Entry";
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
PaymentMatchingDetails: Record "Payment Matching Details";
TempBankStatementMatchingBufferCopy: Record "Bank Statement Matching Buffer" temporary;
BankAccEntrySetReconNo: Codeunit "Bank Acc. Entry Set Recon.-No.";
LineNoFilterTxt: Text;
LineCount: Integer;
AppliedEntryNumbers: List of [Integer];
begin
TempBankStatementMatchingBuffer.Reset();
if TempBankStatementMatchingBuffer.FindSet() then
repeat
TempBankStatementMatchingBufferCopy.Copy(TempBankStatementMatchingBuffer);
TempBankStatementMatchingBufferCopy.Insert();
until TempBankStatementMatchingBuffer.Next() = 0;
TempBankStatementMatchingBuffer.Reset();
TempBankStatementMatchingBuffer.SetCurrentKey(Quality);
TempBankStatementMatchingBuffer.Ascending(false);
if TempBankStatementMatchingBuffer.FindSet() then
repeat
if not AppliedEntryNumbers.Contains(TempBankStatementMatchingBuffer."Entry No.") then begin
TempBankStatementMatchingBufferCopy.Reset();
TempBankStatementMatchingBufferCopy.SetRange("Entry No.", TempBankStatementMatchingBuffer."Entry No.");
if TempBankStatementMatchingBufferCopy.Count() > 1 then begin
LineNoFilterTxt := '';
TempBankStatementMatchingBufferCopy.FindSet();
repeat
if LineNoFilterTxt <> '' then
LineNoFilterTxt += '|';
LineNoFilterTxt += Format(TempBankStatementMatchingBufferCopy."Line No.");
until TempBankStatementMatchingBufferCopy.Next() = 0;
BankAccountLedgerEntry.Get(TempBankStatementMatchingBuffer."Entry No.");
BankAccReconciliationLine.Reset();
BankAccReconciliationLine.SetRange("Statement Type", BankAccReconciliationLine."Statement Type"::"Bank Reconciliation");
BankAccReconciliationLine.SetRange("Bank Account No.", BankAccountNo);
BankAccReconciliationLine.SetRange("Statement No.", StatementNo);
BankAccReconciliationLine.SetFilter("Statement Line No.", LineNoFilterTxt);
LineCount := BankAccReconciliationLine.Count();
if LineCount > 1 then begin
BankAccEntrySetReconNo.SetLineCount(LineCount);
BankAccEntrySetReconNo.SetLineNumber(0);
BankAccEntrySetReconNo.SetAppliedAmount(0);
if BankAccReconciliationLine.FindSet() then
repeat
if BankAccEntrySetReconNo.ApplyEntries(BankAccReconciliationLine, BankAccountLedgerEntry, Relation::"Many-to-One") then begin
TempBankStatementMatchingBufferCopy.Reset();
TempBankStatementMatchingBufferCopy.Get(BankAccReconciliationLine."Statement Line No.", BankAccountLedgerEntry."Entry No.", TempBankStatementMatchingBuffer."Account Type", TempBankStatementMatchingBuffer."Account No.");
PaymentMatchingDetails.CreatePaymentMatchingDetail(BankAccReconciliationLine, TempBankStatementMatchingBufferCopy."Match Details");
end;
until BankAccReconciliationLine.Next() = 0;
AppliedEntryNumbers.Add(TempBankStatementMatchingBuffer."Entry No.");
TempBankStatementMatchingBuffer.Delete();
end;
end;
end else
TempBankStatementMatchingBuffer.Delete();
until TempBankStatementMatchingBuffer.Next() = 0;
end;
procedure ShowMatchSummary(BankAccReconciliation: Record "Bank Acc. Reconciliation")
var
BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line";
FinalText: Text;
AdditionalText: Text;
TotalCount: Integer;
MatchedCount: Integer;
IsHandled: Boolean;
begin
BankAccReconciliationLine.SetRange("Bank Account No.", BankAccReconciliation."Bank Account No.");
BankAccReconciliationLine.SetRange("Statement Type", BankAccReconciliation."Statement Type");
BankAccReconciliationLine.SetRange("Statement No.", BankAccReconciliation."Statement No.");
TotalCount := BankAccReconciliationLine.Count();
BankAccReconciliationLine.SetFilter("Applied Entries", '<>%1', 0);
MatchedCount := BankAccReconciliationLine.Count();
Session.LogMessage('0000KML', StrSubstNo(BankAccountRecTotalAndMatchedLinesLbl, TotalCount, MatchedCount), Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', BankAccountRecCategoryLbl);
if MatchedCount < TotalCount then
AdditionalText := StrSubstNo(MissingMatchMsg, Format(GetMatchLengthTreshold()));
FinalText := StrSubstNo(MatchSummaryMsg, MatchedCount, TotalCount) + AdditionalText;
IsHandled := false;
OnShowMatchSummaryOnAfterSetFinalText(BankAccReconciliation, FinalText, IsHandled);
if not IsHandled then
Message(FinalText);
end;
procedure GetDescriptionMatchScore(BankRecDescription: Text; BankEntryDescription: Text; DocumentNo: Code[20]; ExternalDocumentNo: Code[35]): Integer
var
RecordMatchMgt: Codeunit "Record Match Mgt.";
Nearness: Integer;
Score: Integer;
MatchLengthTreshold: Integer;
NormalizingFactor: Integer;
begin
BankRecDescription := RecordMatchMgt.Trim(BankRecDescription);
BankEntryDescription := RecordMatchMgt.Trim(BankEntryDescription);
MatchLengthTreshold := GetMatchLengthTreshold();
NormalizingFactor := GetNormalizingFactor();
Score := 0;
Nearness := RecordMatchMgt.CalculateStringNearness(BankRecDescription, DocumentNo,
MatchLengthTreshold, NormalizingFactor);
if Nearness = NormalizingFactor then
Score += 11;
Nearness := RecordMatchMgt.CalculateStringNearness(BankRecDescription, ExternalDocumentNo,
MatchLengthTreshold, NormalizingFactor);
if Nearness = NormalizingFactor then
Score += Nearness;
Nearness := RecordMatchMgt.CalculateStringNearness(BankRecDescription, BankEntryDescription,
MatchLengthTreshold, NormalizingFactor);
if Nearness >= 0.8 * NormalizingFactor then
Score += Nearness;
exit(Score);
end;
procedure SetMatchLengthTreshold(NewMatchLengthThreshold: Integer)
begin
MatchLengthTreshold := NewMatchLengthThreshold;
end;
procedure SetNormalizingFactor(NewNormalizingFactor: Integer)
begin
NormalizingFactor := NewNormalizingFactor;
end;
procedure GetMatchLengthTreshold(): Integer
begin
exit(MatchLengthTreshold);
end;
procedure GetNormalizingFactor(): Integer
begin
exit(NormalizingFactor);
end;
/// <summary>
/// Integration event raised after setting the final text in the match summary display.
/// Allows subscribers to customize the summary text or provide alternative display handling.
/// </summary>
/// <param name="BankAccReconciliation">Bank reconciliation document being summarized.</param>
/// <param name="FinalText">The final summary text that will be displayed to the user.</param>
/// <param name="IsHandled">Set to true if subscriber handles the text display processing.</param>
[IntegrationEvent(false, false)]
local procedure OnShowMatchSummaryOnAfterSetFinalText(var BankAccReconciliation: Record "Bank Acc. Reconciliation"; FinalText: Text; var IsHandled: Boolean)
begin
end;
/// <summary>
/// Integration event for finding best matches using custom algorithms or external services.
/// Allows subscribers to implement alternative matching logic, potentially with AI or machine learning.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line to find matches for.</param>
/// <param name="TempBankAccLedgerEntryMatchingBuffer">Buffer containing potential bank account ledger entry matches.</param>
/// <param name="DaysTolerance">Number of days tolerance for date matching.</param>
/// <param name="TempBankStatementMatchingBuffer">Buffer for storing and ranking matching results.</param>
/// <param name="RemovedPreviouslyAssigned">Indicates if previously assigned matches were removed.</param>
/// <param name="Handled">Set to true if subscriber handles the matching process completely.</param>
[IntegrationEvent(false, false)]
local procedure OnFindBestMatches(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var TempBankAccLedgerEntryMatchingBuffer: Record "Ledger Entry Matching Buffer" temporary; DaysTolerance: Integer; var TempBankStatementMatchingBuffer: Record "Bank Statement Matching Buffer" temporary; var RemovedPreviouslyAssigned: Boolean; var Handled: Boolean)
begin
end;
/// <summary>
/// Integration event raised before removing matches from reconciliation lines.
/// Allows subscribers to implement custom removal logic or prevent the removal operation.
/// </summary>
/// <param name="IsHandled">Set to true if subscriber handles the match removal process.</param>
[IntegrationEvent(false, false)]
local procedure OnBeforeRemoveMatchesFromRecLines(var IsHandled: Boolean)
begin
end;
/// <summary>
/// Integration event raised after removing an application from a bank account ledger entry during match removal.
/// Allows subscribers to perform cleanup operations or additional processing after application removal.
/// </summary>
/// <param name="BankAccountLedgerEntry">Bank account ledger entry from which application was removed.</param>
[IntegrationEvent(false, false)]
local procedure OnRemoveMatchOnAfterRemoveApplication(var BankAccountLedgerEntry: Record "Bank Account Ledger Entry");
begin
end;
/// <summary>
/// Integration event raised before applying entries in a many-to-one matching scenario.
/// Allows subscribers to perform validation or modify data before the many-to-one application.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line being applied to multiple entries.</param>
/// <param name="BankAccountLedgerEntry">One of the bank account ledger entries being matched.</param>
[IntegrationEvent(false, false)]
local procedure OnPerformManyToOneMatchOnBeforeApplyEntries(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var BankAccountLedgerEntry: Record "Bank Account Ledger Entry")
begin
end;
/// <summary>
/// Integration event raised after applying entries in a many-to-one matching scenario.
/// Allows subscribers to perform post-application processing or logging for many-to-one matches.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line that was applied to multiple entries.</param>
/// <param name="BankAccountLedgerEntry">One of the bank account ledger entries that was matched.</param>
[IntegrationEvent(false, false)]
local procedure OnPerformManyToOneMatchOnAfterApplyEntries(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var BankAccountLedgerEntry: Record "Bank Account Ledger Entry")
begin
end;
/// <summary>
/// Integration event raised before applying entries in one-to-one or one-to-many matching scenarios.
/// Allows subscribers to perform validation or modify data before the entry application.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line being applied.</param>
/// <param name="BankAccountLedgerEntry">Bank account ledger entry being matched.</param>
[IntegrationEvent(false, false)]
local procedure OnPerformOneToOneOrManyMatchOnBeforeApplyEntries(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var BankAccountLedgerEntry: Record "Bank Account Ledger Entry")
begin
end;
/// <summary>
/// Integration event raised after applying entries in one-to-one or one-to-many matching scenarios.
/// Allows subscribers to perform post-application processing or logging for standard matches.
/// </summary>
/// <param name="BankAccReconciliationLine">Bank reconciliation line that was applied.</param>
/// <param name="BankAccountLedgerEntry">Bank account ledger entry that was matched.</param>
[IntegrationEvent(false, false)]
local procedure OnPerformOneToOneOrManyMatchOnAfterApplyEntries(var BankAccReconciliationLine: Record "Bank Acc. Reconciliation Line"; var BankAccountLedgerEntry: Record "Bank Account Ledger Entry")
begin
end;
}