From ced97ccb9a575051ab5abb179eeafbc9e5b80668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Wed, 10 Jun 2026 13:02:29 +0200 Subject: [PATCH 1/6] single pass over the buffer for the post-processing that computes totals for Totaling accounts --- .../src/Financials/TrialBalance.Codeunit.al | 154 ++++++++++++------ .../src/TrialBalanceExcelReports.Codeunit.al | 57 +++++++ 2 files changed, 158 insertions(+), 53 deletions(-) diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index eafe2f3269..ee9654c53c 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -191,7 +191,8 @@ codeunit 4410 "Trial Balance" #region Query-based approach local procedure InsertTrialBalanceReportDataFromQueries(var GLAccount: Record "G/L Account"; var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary; var TrialBalanceData: Record "EXR Trial Balance Buffer") var - LocalGLAccount: Record "G/L Account"; + TempTotalsBuffer: Record "EXR Trial Balance Buffer" temporary; + AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]; AccountNoFilter: Text; StartDate, EndDate : Date; begin @@ -210,16 +211,12 @@ codeunit 4410 "Trial Balance" if GlobalIncludeBudgetData then InsertBudgetDataFromQuery(GLAccount, TrialBalanceData, StartDate, EndDate); - // The query will just return entries for the "Posting" G/L Accounts and nothing for the Total/End-Total accounts, - // to address that, we calculate the sums from the contents that we now have in the temporary TrialBalanceData table - LocalGLAccount.SetFilter("Account Type", '%1|%2', "G/L Account Type"::"End-Total", "G/L Account Type"::Total); - if AccountNoFilter <> '' then - LocalGLAccount.SetFilter("No.", AccountNoFilter); - if LocalGLAccount.FindSet() then - repeat - if LocalGLAccount.Totaling <> '' then - InsertTotalAccountsFromBuffer(LocalGLAccount, TrialBalanceData); - until LocalGLAccount.Next() = 0; + // The query only returns rows for the "Posting" G/L Accounts and nothing for the Total/End-Total accounts. + // We synthesize them in a single pass over the buffer: first map each posting account to the totals whose + // Totaling range contains it, then sweep the posting rows once, adding each row to every containing total. + BuildAccountToTotalsMap(AccountNoFilter, AccountToTotals); + DistributePostingRowsToTotals(TrialBalanceData, AccountToTotals, TempTotalsBuffer); + MergeTotalsIntoBuffer(TempTotalsBuffer, TrialBalanceData); end; local procedure InsertTrialBalanceFromQuery(var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary; var TrialBalanceData: Record "EXR Trial Balance Buffer"; StartDate: Date; EndDate: Date; AccountNoFilter: Text) @@ -360,57 +357,108 @@ codeunit 4410 "Trial Balance" end; end; - local procedure InsertTotalAccountsFromBuffer(var TotalAccount: Record "G/L Account"; var TrialBalanceData: Record "EXR Trial Balance Buffer") + local procedure BuildAccountToTotalsMap(AccountNoFilter: Text; var AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]) var - TempDimCombinations: Record "EXR Trial Balance Buffer" temporary; + TotalAccount: Record "G/L Account"; + PostingAccount: Record "G/L Account"; begin - Clear(TrialBalanceData); - TrialBalanceData.SetFilter("G/L Account No.", TotalAccount.Totaling); - if not TrialBalanceData.FindSet() then begin - Clear(TrialBalanceData); + // For each Total/End-Total account we record which posting accounts fall inside its Totaling range. + TotalAccount.SetFilter("Account Type", '%1|%2', "G/L Account Type"::"End-Total", "G/L Account Type"::Total); + if AccountNoFilter <> '' then + TotalAccount.SetFilter("No.", AccountNoFilter); + if not TotalAccount.FindSet() then exit; + repeat + if TotalAccount.Totaling <> '' then begin + PostingAccount.Reset(); + PostingAccount.SetRange("Account Type", "G/L Account Type"::Posting); + PostingAccount.SetFilter("No.", TotalAccount.Totaling); + if PostingAccount.FindSet() then + repeat + AddTotalForAccount(AccountToTotals, PostingAccount."No.", TotalAccount."No."); + until PostingAccount.Next() = 0; + end; + until TotalAccount.Next() = 0; + end; + + local procedure AddTotalForAccount(var AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]; PostingAccountNo: Code[20]; TotalAccountNo: Code[20]) + var + ContainingTotals: List of [Code[20]]; + begin + if AccountToTotals.Get(PostingAccountNo, ContainingTotals) then + ContainingTotals.Add(TotalAccountNo) + else begin + ContainingTotals.Add(TotalAccountNo); + AccountToTotals.Add(PostingAccountNo, ContainingTotals); end; + end; - // Collect distinct (Dimension 1, Dimension 2, Business Unit Code) combinations from the loaded records in the totaling range + local procedure DistributePostingRowsToTotals(var TrialBalanceData: Record "EXR Trial Balance Buffer"; var AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]; var TotalsBuffer: Record "EXR Trial Balance Buffer") + var + ContainingTotals: List of [Code[20]]; + TotalAccountNo: Code[20]; + begin + // Single sweep over the posting rows; each row is added once to every total whose range contains its account. + TrialBalanceData.Reset(); + if not TrialBalanceData.FindSet() then + exit; repeat - TempDimCombinations."G/L Account No." := TotalAccount."No."; - TempDimCombinations."Dimension 1 Code" := TrialBalanceData."Dimension 1 Code"; - TempDimCombinations."Dimension 2 Code" := TrialBalanceData."Dimension 2 Code"; - TempDimCombinations."Business Unit Code" := TrialBalanceData."Business Unit Code"; - if not TempDimCombinations.Insert() then; + if AccountToTotals.Get(TrialBalanceData."G/L Account No.", ContainingTotals) then + foreach TotalAccountNo in ContainingTotals do + AddRowToTotal(TotalAccountNo, TrialBalanceData, TotalsBuffer); until TrialBalanceData.Next() = 0; + end; - // For each combination, compute the sums (in memory) and insert an total record - if TempDimCombinations.FindSet() then - repeat - Clear(TrialBalanceData); - TrialBalanceData.SetFilter("G/L Account No.", TotalAccount.Totaling); - TrialBalanceData.SetRange("Dimension 1 Code", TempDimCombinations."Dimension 1 Code"); - TrialBalanceData.SetRange("Dimension 2 Code", TempDimCombinations."Dimension 2 Code"); - TrialBalanceData.SetRange("Business Unit Code", TempDimCombinations."Business Unit Code"); - TrialBalanceData.CalcSums( - // LCY - "Net Change", "Net Change (Debit)", "Net Change (Credit)", - Balance, "Balance (Debit)", "Balance (Credit)", - "Starting Balance", "Starting Balance (Debit)", "Starting Balance (Credit)", - // ACY - "Net Change (ACY)", "Net Change (Debit) (ACY)", "Net Change (Credit) (ACY)", - "Balance (ACY)", "Balance (Debit) (ACY)", "Balance (Credit) (ACY)", - "Starting Balance (ACY)", "Starting Balance (Debit) (ACY)", "Starting Balance (Credit)(ACY)", - // Budget - "Budget (Net)", "Budget (Bal. at Date)" - ); - TrialBalanceData."G/L Account No." := TotalAccount."No."; - TrialBalanceData."Dimension 1 Code" := TempDimCombinations."Dimension 1 Code"; - TrialBalanceData."Dimension 2 Code" := TempDimCombinations."Dimension 2 Code"; - TrialBalanceData."Business Unit Code" := TempDimCombinations."Business Unit Code"; - TrialBalanceData.CalculateBudgetComparisons(); - TrialBalanceData.CheckAllZero(); - if not TrialBalanceData."All Zero" then - TrialBalanceData.Insert(true); - until TempDimCombinations.Next() = 0; + local procedure AddRowToTotal(TotalAccountNo: Code[20]; var SourceRow: Record "EXR Trial Balance Buffer"; var TotalsBuffer: Record "EXR Trial Balance Buffer") + begin + if not TotalsBuffer.Get(TotalAccountNo, SourceRow."Dimension 1 Code", SourceRow."Dimension 2 Code", SourceRow."Business Unit Code", SourceRow."Period Start") then begin + TotalsBuffer.Init(); + TotalsBuffer."G/L Account No." := TotalAccountNo; + TotalsBuffer."Dimension 1 Code" := SourceRow."Dimension 1 Code"; + TotalsBuffer."Dimension 2 Code" := SourceRow."Dimension 2 Code"; + TotalsBuffer."Business Unit Code" := SourceRow."Business Unit Code"; + TotalsBuffer."Period Start" := SourceRow."Period Start"; + TotalsBuffer.Insert(); + end; + // LCY + TotalsBuffer."Net Change" := TotalsBuffer."Net Change" + SourceRow."Net Change"; + TotalsBuffer."Net Change (Debit)" := TotalsBuffer."Net Change (Debit)" + SourceRow."Net Change (Debit)"; + TotalsBuffer."Net Change (Credit)" := TotalsBuffer."Net Change (Credit)" + SourceRow."Net Change (Credit)"; + TotalsBuffer.Balance := TotalsBuffer.Balance + SourceRow.Balance; + TotalsBuffer."Balance (Debit)" := TotalsBuffer."Balance (Debit)" + SourceRow."Balance (Debit)"; + TotalsBuffer."Balance (Credit)" := TotalsBuffer."Balance (Credit)" + SourceRow."Balance (Credit)"; + TotalsBuffer."Starting Balance" := TotalsBuffer."Starting Balance" + SourceRow."Starting Balance"; + TotalsBuffer."Starting Balance (Debit)" := TotalsBuffer."Starting Balance (Debit)" + SourceRow."Starting Balance (Debit)"; + TotalsBuffer."Starting Balance (Credit)" := TotalsBuffer."Starting Balance (Credit)" + SourceRow."Starting Balance (Credit)"; + // ACY + TotalsBuffer."Net Change (ACY)" := TotalsBuffer."Net Change (ACY)" + SourceRow."Net Change (ACY)"; + TotalsBuffer."Net Change (Debit) (ACY)" := TotalsBuffer."Net Change (Debit) (ACY)" + SourceRow."Net Change (Debit) (ACY)"; + TotalsBuffer."Net Change (Credit) (ACY)" := TotalsBuffer."Net Change (Credit) (ACY)" + SourceRow."Net Change (Credit) (ACY)"; + TotalsBuffer."Balance (ACY)" := TotalsBuffer."Balance (ACY)" + SourceRow."Balance (ACY)"; + TotalsBuffer."Balance (Debit) (ACY)" := TotalsBuffer."Balance (Debit) (ACY)" + SourceRow."Balance (Debit) (ACY)"; + TotalsBuffer."Balance (Credit) (ACY)" := TotalsBuffer."Balance (Credit) (ACY)" + SourceRow."Balance (Credit) (ACY)"; + TotalsBuffer."Starting Balance (ACY)" := TotalsBuffer."Starting Balance (ACY)" + SourceRow."Starting Balance (ACY)"; + TotalsBuffer."Starting Balance (Debit) (ACY)" := TotalsBuffer."Starting Balance (Debit) (ACY)" + SourceRow."Starting Balance (Debit) (ACY)"; + TotalsBuffer."Starting Balance (Credit)(ACY)" := TotalsBuffer."Starting Balance (Credit)(ACY)" + SourceRow."Starting Balance (Credit)(ACY)"; + // Budget + TotalsBuffer."Budget (Net)" := TotalsBuffer."Budget (Net)" + SourceRow."Budget (Net)"; + TotalsBuffer."Budget (Bal. at Date)" := TotalsBuffer."Budget (Bal. at Date)" + SourceRow."Budget (Bal. at Date)"; + TotalsBuffer.Modify(); + end; - Clear(TrialBalanceData); + local procedure MergeTotalsIntoBuffer(var TotalsBuffer: Record "EXR Trial Balance Buffer"; var TrialBalanceData: Record "EXR Trial Balance Buffer") + begin + TrialBalanceData.Reset(); + TotalsBuffer.Reset(); + if not TotalsBuffer.FindSet() then + exit; + repeat + TrialBalanceData := TotalsBuffer; + TrialBalanceData.CalculateBudgetComparisons(); + TrialBalanceData.CheckAllZero(); + if not TrialBalanceData."All Zero" then + TrialBalanceData.Insert(true); + until TotalsBuffer.Next() = 0; end; local procedure InsertBudgetDataFromQuery(var GLAccount: Record "G/L Account"; var TrialBalanceData: Record "EXR Trial Balance Buffer"; StartDate: Date; EndDate: Date) diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index b7bb3a1e21..bc9389bd7f 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -477,6 +477,48 @@ codeunit 139544 "Trial Balance Excel Reports" Assert.AreEqual(Amount1Dim1 + Amount2Dim1, TempTrialBalanceData.Balance, 'Total Dim2=Value1 should sum both posting accounts'); end; + [Test] + procedure QueryPathDoesNotDoubleCountNestedTotals() + var + GLAccount: Record "G/L Account"; + TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; + TrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + PostingAccountNo, ChildTotalNo, ParentTotalNo : Code[20]; + EntryAmount: Decimal; + begin + // [SCENARIO] A parent End-Total whose Totaling range contains a nested child End-Total must not double-count the child's amounts. + // [GIVEN] A posting account (10000), a child End-Total (20000) totaling the posting account, + // and a parent End-Total (30000) whose range 10000..29999 spans BOTH the posting account and the child End-Total's number. + // The total accounts are processed in No. order, so the child (20000) is inserted into the buffer before the parent (30000) is computed. + Initialize(); + PostingAccountNo := CreateGLAccountWithNo('10000', Enum::"G/L Account Type"::Posting, ''); + ChildTotalNo := CreateGLAccountWithNo('20000', Enum::"G/L Account Type"::"End-Total", '10000..19999'); + ParentTotalNo := CreateGLAccountWithNo('30000', Enum::"G/L Account Type"::"End-Total", '10000..29999'); + + // [GIVEN] A single entry posted to the posting account + EntryAmount := 1000; + CreateGLEntryWithAmount(PostingAccountNo, '', '', '', WorkDate(), EntryAmount); + + // [WHEN] Running the query-based trial balance for the current year + GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); + TrialBalance.ConfigureTrialBalance(false, false); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + + // [THEN] The child End-Total equals the entry amount (the posting account counted once) + TrialBalanceData.Reset(); + TrialBalanceData.SetRange("G/L Account No.", ChildTotalNo); + TrialBalanceData.FindFirst(); + Assert.AreEqual(EntryAmount, TrialBalanceData.Balance, 'Child End-Total should sum the posting account once'); + + // [THEN] The parent End-Total ALSO equals the entry amount, not twice: + // its Totaling range includes the child End-Total's already-inserted buffer row, which must not be re-summed. + TrialBalanceData.Reset(); + TrialBalanceData.SetRange("G/L Account No.", ParentTotalNo); + TrialBalanceData.FindFirst(); + Assert.AreEqual(EntryAmount, TrialBalanceData.Balance, 'Parent End-Total must not double-count the nested child End-Total'); + end; + [Test] procedure QueryPathPopulatesBudgetFields() var @@ -820,6 +862,21 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.Modify(); end; + local procedure CreateGLAccountWithNo(No: Code[20]; AccountType: Enum "G/L Account Type"; Totaling: Text): Code[20] + var + GLAccount: Record "G/L Account"; + begin + // Insert with an explicit No. so the test controls both the FindSet (No.) ordering of the + // total accounts and whether one total's number falls inside another total's Totaling range. + GLAccount.Init(); + GLAccount."No." := No; + GLAccount.Name := No; + GLAccount."Account Type" := AccountType; + GLAccount.Totaling := CopyStr(Totaling, 1, MaxStrLen(GLAccount.Totaling)); + GLAccount.Insert(); + exit(No); + end; + local procedure Initialize() var GLAccount: Record "G/L Account"; From d0cd74e7242cd08232926f5cce65c56e58663a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Thu, 11 Jun 2026 16:35:58 +0200 Subject: [PATCH 2/6] Starting date can be a closing date --- .../src/Financials/TrialBalance.Codeunit.al | 12 +++++- .../src/TrialBalanceExcelReports.Codeunit.al | 41 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index eafe2f3269..2094c5b9c8 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -260,7 +260,7 @@ codeunit 4410 "Trial Balance" EXRTrialBalanceQuery.Close(); // And now we get the balances at the starting date and modify the ones we have already inserted - EXRTrialBalanceQuery.SetFilter(EXRTrialBalanceQuery.PostingDate, '..%1', ClosingDate(StartDate - 1)); + EXRTrialBalanceQuery.SetFilter(EXRTrialBalanceQuery.PostingDate, '..%1', GetOpeningBalanceCutoff(StartDate)); EXRTrialBalanceQuery.Open(); while EXRTrialBalanceQuery.Read() do begin TrialBalanceData.SetRange("G/L Account No.", EXRTrialBalanceQuery.AccountNumber); @@ -328,7 +328,7 @@ codeunit 4410 "Trial Balance" EXRTrialBalanceBUQuery.Close(); // And now we get the balances at the starting date and modify the ones we have already inserted - EXRTrialBalanceBUQuery.SetFilter(EXRTrialBalanceBUQuery.PostingDate, '..%1', ClosingDate(StartDate - 1)); + EXRTrialBalanceBUQuery.SetFilter(EXRTrialBalanceBUQuery.PostingDate, '..%1', GetOpeningBalanceCutoff(StartDate)); EXRTrialBalanceBUQuery.Open(); while EXRTrialBalanceBUQuery.Read() do begin TrialBalanceData.SetRange("G/L Account No.", EXRTrialBalanceBUQuery.AccountNumber); @@ -491,6 +491,14 @@ codeunit 4410 "Trial Balance" if EndDate = 0D then EndDate := StartDate; end; + + local procedure GetOpeningBalanceCutoff(StartDate: Date): Date + begin + // We return the date immediately before the starting date, considering BC's date ordering and the presence of closing dates + if StartDate = ClosingDate(StartDate) then + exit(NormalDate(StartDate)); + exit(ClosingDate(StartDate - 1)); + end; #endregion #if not CLEAN27 diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index b7bb3a1e21..9b870920bb 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -667,6 +667,47 @@ codeunit 139544 "Trial Balance Excel Reports" Assert.AreEqual(0, TempTrialBalanceData."Starting Balance", 'Starting Balance should be zero after closing entries') end; + [Test] + procedure QueryPathSupportsClosingDateAsStartingDate() + var + GLAccount: Record "G/L Account"; + TempDimensionValue: Record "Dimension Value" temporary; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + PostingAccount: Code[20]; + PriorYearActivity, ClosingAmount, CurrentYearActivity : Decimal; + PriorYear, CurrentYear : Integer; + begin + // [SCENARIO 638353] The query path supports a closing date as the starting date instead of crashing, + // and includes that day's closing entries in the period rather than the opening balance. + // [GIVEN] A posting account with prior-year activity, a year-end closing entry, and current-year activity + Initialize(); + CreateGLAccount(GLAccount); + PostingAccount := GLAccount."No."; + CurrentYear := Date2DMY(WorkDate(), 3); + PriorYear := CurrentYear - 1; + PriorYearActivity := 5000; + ClosingAmount := -2000; + CurrentYearActivity := 300; + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(15, 6, PriorYear), PriorYearActivity); + CreateGLEntryWithAmount(PostingAccount, '', '', '', ClosingDate(DMY2Date(31, 12, PriorYear)), ClosingAmount); + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(15, 6, CurrentYear), CurrentYearActivity); + + // [WHEN] Running the trial balance with the starting date set to the prior year's closing date + GLAccount.SetRange("No.", PostingAccount); + GLAccount.SetRange("Date Filter", ClosingDate(DMY2Date(31, 12, PriorYear)), DMY2Date(31, 12, CurrentYear)); + TrialBalance.ConfigureTrialBalance(false, false); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimensionValue, TempDimensionValue, TempTrialBalanceData); + + // [THEN] The opening balance holds only the activity strictly before the closing date + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the posting account'); + Assert.AreEqual(PriorYearActivity, TempTrialBalanceData."Starting Balance", 'Starting Balance should exclude the closing-date entry'); + // [THEN] The closing-date entry falls inside the reported period together with current-year activity + Assert.AreEqual(ClosingAmount + CurrentYearActivity, TempTrialBalanceData."Net Change", 'Net Change should include the closing-date entry'); + Assert.AreEqual(PriorYearActivity + ClosingAmount + CurrentYearActivity, TempTrialBalanceData.Balance, 'Balance should equal Starting Balance + Net Change'); + end; + [Test] [HandlerFunctions('EXRAgedAccPayableExcelHandler')] procedure AgedAccountsPayableExportsDocumentTypeAndNo() From c45cf9f6a65cbc60af5d4c28a2c48e95441b413e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Tue, 16 Jun 2026 12:05:53 +0200 Subject: [PATCH 3/6] Fix AA0073: prefix temporary buffer variable with Temp Rename TrialBalanceData to TempTrialBalanceData in QueryPathDoesNotDoubleCountNestedTotals to match the temporary-variable naming convention enforced by AA0073. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/TrialBalanceExcelReports.Codeunit.al | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index 2bad87779e..58a4383a83 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -474,7 +474,7 @@ codeunit 139544 "Trial Balance Excel Reports" var GLAccount: Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; PostingAccountNo, ChildTotalNo, ParentTotalNo : Code[20]; EntryAmount: Decimal; @@ -495,20 +495,20 @@ codeunit 139544 "Trial Balance Excel Reports" // [WHEN] Running the query-based trial balance for the current year GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] The child End-Total equals the entry amount (the posting account counted once) - TrialBalanceData.Reset(); - TrialBalanceData.SetRange("G/L Account No.", ChildTotalNo); - TrialBalanceData.FindFirst(); - Assert.AreEqual(EntryAmount, TrialBalanceData.Balance, 'Child End-Total should sum the posting account once'); + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", ChildTotalNo); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(EntryAmount, TempTrialBalanceData.Balance, 'Child End-Total should sum the posting account once'); // [THEN] The parent End-Total ALSO equals the entry amount, not twice: // its Totaling range includes the child End-Total's already-inserted buffer row, which must not be re-summed. - TrialBalanceData.Reset(); - TrialBalanceData.SetRange("G/L Account No.", ParentTotalNo); - TrialBalanceData.FindFirst(); - Assert.AreEqual(EntryAmount, TrialBalanceData.Balance, 'Parent End-Total must not double-count the nested child End-Total'); + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", ParentTotalNo); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(EntryAmount, TempTrialBalanceData.Balance, 'Parent End-Total must not double-count the nested child End-Total'); end; [Test] From 470ad7c87a651302523112879adf8a7407c053f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Fri, 26 Jun 2026 11:48:53 +0200 Subject: [PATCH 4/6] fix for edge case when balance of a posting account at ending date is zero --- .../src/Financials/TrialBalance.Codeunit.al | 6 ++- .../src/TrialBalanceExcelReports.Codeunit.al | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index 268ad055ff..9a9c858853 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -263,7 +263,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData.SetRange("G/L Account No.", EXRTrialBalanceQuery.AccountNumber); TrialBalanceData.SetRange("Dimension 1 Code", EXRTrialBalanceQuery.DimensionValue1Code); TrialBalanceData.SetRange("Dimension 2 Code", EXRTrialBalanceQuery.DimensionValue2Code); - if not TrialBalanceData.FindFirst() then begin // This shouldn't happen, but we consider it regardless + if not TrialBalanceData.FindFirst() then begin + // The combination nets to zero at the end date, so the first pass skipped it as All Zero, but it still has an opening balance here. + TrialBalanceData.Init(); TrialBalanceData."G/L Account No." := EXRTrialBalanceQuery.AccountNumber; TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceQuery.DimensionValue1Code; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceQuery.DimensionValue2Code; @@ -333,6 +335,8 @@ codeunit 4410 "Trial Balance" TrialBalanceData.SetRange("Dimension 2 Code", EXRTrialBalanceBUQuery.DimensionValue2Code); TrialBalanceData.SetRange("Business Unit Code", EXRTrialBalanceBUQuery.BusinessUnitCode); if not TrialBalanceData.FindFirst() then begin + // The combination nets to zero at the end date, so the first pass skipped it as All Zero, but it still has an opening balance here. + TrialBalanceData.Init(); TrialBalanceData."G/L Account No." := EXRTrialBalanceBUQuery.AccountNumber; TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceBUQuery.DimensionValue1Code; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceBUQuery.DimensionValue2Code; diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index 58a4383a83..126d4ed05d 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -665,6 +665,51 @@ codeunit 139544 "Trial Balance Excel Reports" Assert.AreEqual(NonZeroAccount, TempTrialBalanceData."G/L Account No.", 'The non-zero account should be the one returned'); end; + [Test] + procedure QueryPathDoesNotInheritStaleAmountsForZeroNetCombination() + var + GLAccount: Record "G/L Account"; + TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + ZeroNetAccountNo, StaleSourceAccountNo : Code[20]; + OpeningAmount, StaleBalance : Decimal; + PeriodStart: Date; + begin + // [SCENARIO] A combination that nets to zero at the end date but still has an opening balance is dropped by the first + // query pass (All Zero) and re-created by the second (opening-balance) pass. That re-created record must be + // Init()'d, otherwise it inherits the stale amounts left on the shared buffer record by the previous account. + Initialize(); + PeriodStart := DMY2Date(1, 1, Date2DMY(WorkDate(), 3)); + + // [GIVEN] A zero-net posting account (10000) read first in the second pass, and a stale-source posting account + // (20000) read last in the first pass so it leaves a non-zero balance on the shared buffer record. + ZeroNetAccountNo := CreateGLAccountWithNo('10000', Enum::"G/L Account Type"::Posting, ''); + StaleSourceAccountNo := CreateGLAccountWithNo('20000', Enum::"G/L Account Type"::Posting, ''); + + OpeningAmount := 1000; + StaleBalance := 777; + // [GIVEN] 10000 has an opening balance before the period that is fully reversed within the period -> end balance 0 + CreateGLEntryWithAmount(ZeroNetAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3) - 1), OpeningAmount); + CreateGLEntryWithAmount(ZeroNetAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), -OpeningAmount); + // [GIVEN] 20000 has only in-period activity (non-zero end balance, no opening balance), so it is absent from the second pass + CreateGLEntryWithAmount(StaleSourceAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), StaleBalance); + + // [WHEN] Running the query-based trial balance for the current year + GLAccount.SetRange("Date Filter", PeriodStart, DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); + TrialBalance.ConfigureTrialBalance(false, false); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); + + // [THEN] The zero-net account reports its own amounts (Starting Balance = opening, Net Change = -opening, Balance = 0), + // not the stale source account's balance carried over from the previous record. + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", ZeroNetAccountNo); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the zero-net account'); + Assert.AreEqual(OpeningAmount, TempTrialBalanceData."Starting Balance", 'Starting Balance should equal the opening balance'); + Assert.AreEqual(-OpeningAmount, TempTrialBalanceData."Net Change", 'Net Change should reverse the opening balance, not inherit a stale net change'); + Assert.AreEqual(0, TempTrialBalanceData.Balance, 'Balance should be zero, not the stale source account balance'); + end; + [Test] procedure QueryPathStartingBalanceIncludesClosingDateEntries() var From d24a08cf73f484a2698e4cf884cda26a7ffff891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Fri, 26 Jun 2026 12:07:26 +0200 Subject: [PATCH 5/6] fix for edge case when balance of a posting account at ending date is zero --- .../src/Financials/TrialBalance.Codeunit.al | 53 ++++++++++------ .../src/TrialBalanceExcelReports.Codeunit.al | 63 ++++++++++--------- 2 files changed, 70 insertions(+), 46 deletions(-) diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index 9a9c858853..7011836c56 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -247,12 +247,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceQuery.ACYAmount; TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceQuery.ACYCreditAmount; - TrialBalanceData.CheckAllZero(); - if not TrialBalanceData."All Zero" then begin - TrialBalanceData.Insert(true); - InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); - InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); - end; + // Insert every combination the query returns, including ones that net to zero at the end date, so their + // debit/credit splits survive for the second pass to adjust. All-zero rows are removed at the end. + TrialBalanceData.Insert(true); end; EXRTrialBalanceQuery.Close(); @@ -264,7 +261,7 @@ codeunit 4410 "Trial Balance" TrialBalanceData.SetRange("Dimension 1 Code", EXRTrialBalanceQuery.DimensionValue1Code); TrialBalanceData.SetRange("Dimension 2 Code", EXRTrialBalanceQuery.DimensionValue2Code); if not TrialBalanceData.FindFirst() then begin - // The combination nets to zero at the end date, so the first pass skipped it as All Zero, but it still has an opening balance here. + // This shouldn't happen now that the first pass inserts every combination with entries up to the ending date, but we Init() and consider it regardless. TrialBalanceData.Init(); TrialBalanceData."G/L Account No." := EXRTrialBalanceQuery.AccountNumber; TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceQuery.DimensionValue1Code; @@ -286,9 +283,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceQuery.ACYCreditAmount; TrialBalanceData.Modify(); - InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); - InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; + + RemoveAllZeroRowsAndRegisterDimensions(TrialBalanceData, Dimension1Values, Dimension2Values); end; local procedure InsertTrialBalanceFromBUQuery(var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary; var TrialBalanceData: Record "EXR Trial Balance Buffer"; StartDate: Date; EndDate: Date; AccountNoFilter: Text) @@ -317,12 +314,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceBUQuery.ACYAmount; TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceBUQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceBUQuery.ACYCreditAmount; - TrialBalanceData.CheckAllZero(); - if not TrialBalanceData."All Zero" then begin - TrialBalanceData.Insert(true); - InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); - InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); - end; + // Insert every combination the query returns, including ones that net to zero at the end date, so their + // debit/credit splits survive for the second pass to adjust. All-zero rows are removed at the end. + TrialBalanceData.Insert(true); end; EXRTrialBalanceBUQuery.Close(); @@ -335,7 +329,7 @@ codeunit 4410 "Trial Balance" TrialBalanceData.SetRange("Dimension 2 Code", EXRTrialBalanceBUQuery.DimensionValue2Code); TrialBalanceData.SetRange("Business Unit Code", EXRTrialBalanceBUQuery.BusinessUnitCode); if not TrialBalanceData.FindFirst() then begin - // The combination nets to zero at the end date, so the first pass skipped it as All Zero, but it still has an opening balance here. + // This shouldn't happen now that the first pass inserts every combination with entries up to the ending date, but we Init() and consider it regardless. TrialBalanceData.Init(); TrialBalanceData."G/L Account No." := EXRTrialBalanceBUQuery.AccountNumber; TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceBUQuery.DimensionValue1Code; @@ -356,9 +350,32 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceBUQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceBUQuery.ACYCreditAmount; TrialBalanceData.Modify(); - InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); - InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; + + RemoveAllZeroRowsAndRegisterDimensions(TrialBalanceData, Dimension1Values, Dimension2Values); + end; + + local procedure RemoveAllZeroRowsAndRegisterDimensions(var TrialBalanceData: Record "EXR Trial Balance Buffer"; var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary) + begin + // The first query pass inserts every combination that has entries (so the debit/credit splits survive the + // second pass), including ones that net to zero. Now that the net change is final, drop the combinations with + // no activity and register the dimension values used by the rows we keep. + TrialBalanceData.Reset(); + if TrialBalanceData.FindSet() then + repeat + TrialBalanceData.CheckAllZero(); + TrialBalanceData.Modify(); + until TrialBalanceData.Next() = 0; + + TrialBalanceData.SetRange("All Zero", true); + TrialBalanceData.DeleteAll(); + TrialBalanceData.SetRange("All Zero"); + + if TrialBalanceData.FindSet() then + repeat + InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); + InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); + until TrialBalanceData.Next() = 0; end; local procedure BuildAccountToTotalsMap(AccountNoFilter: Text; var AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]) diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index 126d4ed05d..80b8e4172a 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -666,48 +666,55 @@ codeunit 139544 "Trial Balance Excel Reports" end; [Test] - procedure QueryPathDoesNotInheritStaleAmountsForZeroNetCombination() + procedure QueryPathReportsCorrectDebitCreditSplitsForZeroEndBalance() var - GLAccount: Record "G/L Account"; + GLAccount, KeptAccount, DroppedAccount : Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; - ZeroNetAccountNo, StaleSourceAccountNo : Code[20]; - OpeningAmount, StaleBalance : Decimal; - PeriodStart: Date; + OpeningDebit, OffsettingAmount : Decimal; + PriorYear: Integer; begin - // [SCENARIO] A combination that nets to zero at the end date but still has an opening balance is dropped by the first - // query pass (All Zero) and re-created by the second (opening-balance) pass. That re-created record must be - // Init()'d, otherwise it inherits the stale amounts left on the shared buffer record by the previous account. + // [SCENARIO] Combinations that net to zero at the end date are inserted by the first pass (instead of being + // dropped as All Zero) so the second pass can produce correct debit/credit splits. Combinations with no net + // activity at all are then removed, while those with period activity are kept with correct gross columns. Initialize(); - PeriodStart := DMY2Date(1, 1, Date2DMY(WorkDate(), 3)); + PriorYear := Date2DMY(WorkDate(), 3) - 1; + OpeningDebit := 1000; + OffsettingAmount := 500; - // [GIVEN] A zero-net posting account (10000) read first in the second pass, and a stale-source posting account - // (20000) read last in the first pass so it leaves a non-zero balance on the shared buffer record. - ZeroNetAccountNo := CreateGLAccountWithNo('10000', Enum::"G/L Account Type"::Posting, ''); - StaleSourceAccountNo := CreateGLAccountWithNo('20000', Enum::"G/L Account Type"::Posting, ''); + // [GIVEN] An account whose opening debit is fully reversed by a credit within the period (net-zero end, period activity) + CreateGLAccount(KeptAccount); + CreateGLEntryWithAmount(KeptAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), OpeningDebit); + CreateGLEntryWithAmount(KeptAccount."No.", '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), -OpeningDebit); - OpeningAmount := 1000; - StaleBalance := 777; - // [GIVEN] 10000 has an opening balance before the period that is fully reversed within the period -> end balance 0 - CreateGLEntryWithAmount(ZeroNetAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3) - 1), OpeningAmount); - CreateGLEntryWithAmount(ZeroNetAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), -OpeningAmount); - // [GIVEN] 20000 has only in-period activity (non-zero end balance, no opening balance), so it is absent from the second pass - CreateGLEntryWithAmount(StaleSourceAccountNo, '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), StaleBalance); + // [GIVEN] An account with only offsetting opening turnover and no period activity (net-zero everything) + CreateGLAccount(DroppedAccount); + CreateGLEntryWithAmount(DroppedAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), OffsettingAmount); + CreateGLEntryWithAmount(DroppedAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), -OffsettingAmount); // [WHEN] Running the query-based trial balance for the current year - GLAccount.SetRange("Date Filter", PeriodStart, DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); + GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); - // [THEN] The zero-net account reports its own amounts (Starting Balance = opening, Net Change = -opening, Balance = 0), - // not the stale source account's balance carried over from the previous record. + // [THEN] The active combination is kept with correct net AND gross debit/credit columns + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", KeptAccount."No."); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist; the combination has period activity'); + Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Starting Balance", 'Starting Balance should equal the opening debit'); + Assert.AreEqual(-OpeningDebit, TempTrialBalanceData."Net Change", 'Net Change should reverse the opening'); + Assert.AreEqual(0, TempTrialBalanceData.Balance, 'Balance should net to zero at the end date'); + Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Starting Balance (Debit)", 'Opening debit split'); + Assert.AreEqual(0, TempTrialBalanceData."Net Change (Debit)", 'No period debit turnover'); + Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Net Change (Credit)", 'Period credit turnover equals the reversal'); + Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Balance (Debit)", 'Cumulative debit at the end date'); + Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Balance (Credit)", 'Cumulative credit at the end date'); + + // [THEN] The inactive combination is dropped entirely (previously it surfaced as a phantom stale row) TempTrialBalanceData.Reset(); - TempTrialBalanceData.SetRange("G/L Account No.", ZeroNetAccountNo); - Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the zero-net account'); - Assert.AreEqual(OpeningAmount, TempTrialBalanceData."Starting Balance", 'Starting Balance should equal the opening balance'); - Assert.AreEqual(-OpeningAmount, TempTrialBalanceData."Net Change", 'Net Change should reverse the opening balance, not inherit a stale net change'); - Assert.AreEqual(0, TempTrialBalanceData.Balance, 'Balance should be zero, not the stale source account balance'); + TempTrialBalanceData.SetRange("G/L Account No.", DroppedAccount."No."); + Assert.IsTrue(TempTrialBalanceData.IsEmpty(), 'A combination with no net activity should be dropped'); end; [Test] From 09e70035a3f96aaec95b28ca058bf8bebb3ade32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20Mart=C3=ADnez=20Pineda?= Date: Fri, 26 Jun 2026 13:02:36 +0200 Subject: [PATCH 6/6] fix for edge case when balance of a posting account at ending date is zero --- .../src/Financials/TrialBalance.Codeunit.al | 43 +++++----------- .../src/TrialBalanceExcelReports.Codeunit.al | 51 +++++++++---------- 2 files changed, 35 insertions(+), 59 deletions(-) diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index 7011836c56..32d6a211fb 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -247,9 +247,11 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceQuery.ACYAmount; TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceQuery.ACYCreditAmount; - // Insert every combination the query returns, including ones that net to zero at the end date, so their - // debit/credit splits survive for the second pass to adjust. All-zero rows are removed at the end. + // Every combination the query returns has entries, so it represents real activity and is kept even when it + // nets to zero. The second pass adjusts any that also have an opening balance. TrialBalanceData.Insert(true); + InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); + InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; EXRTrialBalanceQuery.Close(); @@ -283,9 +285,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceQuery.ACYCreditAmount; TrialBalanceData.Modify(); + InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); + InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; - - RemoveAllZeroRowsAndRegisterDimensions(TrialBalanceData, Dimension1Values, Dimension2Values); end; local procedure InsertTrialBalanceFromBUQuery(var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary; var TrialBalanceData: Record "EXR Trial Balance Buffer"; StartDate: Date; EndDate: Date; AccountNoFilter: Text) @@ -314,9 +316,11 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceBUQuery.ACYAmount; TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceBUQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceBUQuery.ACYCreditAmount; - // Insert every combination the query returns, including ones that net to zero at the end date, so their - // debit/credit splits survive for the second pass to adjust. All-zero rows are removed at the end. + // Every combination the query returns has entries, so it represents real activity and is kept even when it + // nets to zero. The second pass adjusts any that also have an opening balance. TrialBalanceData.Insert(true); + InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); + InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; EXRTrialBalanceBUQuery.Close(); @@ -350,32 +354,9 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceBUQuery.ACYDebitAmount; TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceBUQuery.ACYCreditAmount; TrialBalanceData.Modify(); + InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); + InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); end; - - RemoveAllZeroRowsAndRegisterDimensions(TrialBalanceData, Dimension1Values, Dimension2Values); - end; - - local procedure RemoveAllZeroRowsAndRegisterDimensions(var TrialBalanceData: Record "EXR Trial Balance Buffer"; var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary) - begin - // The first query pass inserts every combination that has entries (so the debit/credit splits survive the - // second pass), including ones that net to zero. Now that the net change is final, drop the combinations with - // no activity and register the dimension values used by the rows we keep. - TrialBalanceData.Reset(); - if TrialBalanceData.FindSet() then - repeat - TrialBalanceData.CheckAllZero(); - TrialBalanceData.Modify(); - until TrialBalanceData.Next() = 0; - - TrialBalanceData.SetRange("All Zero", true); - TrialBalanceData.DeleteAll(); - TrialBalanceData.SetRange("All Zero"); - - if TrialBalanceData.FindSet() then - repeat - InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); - InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); - until TrialBalanceData.Next() = 0; end; local procedure BuildAccountToTotalsMap(AccountNoFilter: Text; var AccountToTotals: Dictionary of [Code[20], List of [Code[20]]]) diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index 80b8e4172a..28d9313d53 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -633,24 +633,27 @@ codeunit 139544 "Trial Balance Excel Reports" end; [Test] - procedure QueryPathSkipsAllZeroRecords() + procedure QueryPathIncludesAccountsThatNetToZero() var GLAccount: Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; ZeroAccount, NonZeroAccount : Code[20]; + GrossAmount: Decimal; begin - // [SCENARIO] Accounts with entries that sum to zero are not included in the buffer. - // [GIVEN] One account with cancelling entries (net zero) and another with a non-zero balance + // [SCENARIO] Accounts that have entries are included even when they net to zero. The query only returns + // accounts with activity, so a zero net change still represents real (offsetting) turnover worth showing. + // [GIVEN] One account with cancelling entries (net zero, gross turnover) and another with a non-zero balance Initialize(); LibraryERM.CreateGLAccount(GLAccount); ZeroAccount := GLAccount."No."; LibraryERM.CreateGLAccount(GLAccount); NonZeroAccount := GLAccount."No."; - CreateGLEntryWithAmount(ZeroAccount, '', '', '', WorkDate(), 500); - CreateGLEntryWithAmount(ZeroAccount, '', '', '', WorkDate(), -500); + GrossAmount := 500; + CreateGLEntryWithAmount(ZeroAccount, '', '', '', WorkDate(), GrossAmount); + CreateGLEntryWithAmount(ZeroAccount, '', '', '', WorkDate(), -GrossAmount); CreateGLEntryWithAmount(NonZeroAccount, '', '', '', WorkDate(), 100); // [WHEN] Running the trial balance @@ -659,49 +662,46 @@ codeunit 139544 "Trial Balance Excel Reports" TrialBalance.ConfigureTrialBalance(false, false); TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); - // [THEN] Only the non-zero account appears - Assert.AreEqual(1, TempTrialBalanceData.Count(), 'Only the non-zero account should be in the buffer'); - TempTrialBalanceData.FindFirst(); - Assert.AreEqual(NonZeroAccount, TempTrialBalanceData."G/L Account No.", 'The non-zero account should be the one returned'); + // [THEN] Both accounts are in the buffer + Assert.AreEqual(2, TempTrialBalanceData.Count(), 'Both accounts with entries should be in the buffer'); + // [THEN] The net-zero account is present with zero net change and balance, but its gross turnover is reported + TempTrialBalanceData.SetRange("G/L Account No.", ZeroAccount); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'The net-zero account should be included'); + Assert.AreEqual(0, TempTrialBalanceData."Net Change", 'Net Change should be zero'); + Assert.AreEqual(0, TempTrialBalanceData.Balance, 'Balance should be zero'); + Assert.AreEqual(GrossAmount, TempTrialBalanceData."Net Change (Debit)", 'Gross debit turnover should be reported'); + Assert.AreEqual(GrossAmount, TempTrialBalanceData."Net Change (Credit)", 'Gross credit turnover should be reported'); end; [Test] procedure QueryPathReportsCorrectDebitCreditSplitsForZeroEndBalance() var - GLAccount, KeptAccount, DroppedAccount : Record "G/L Account"; + GLAccount, KeptAccount : Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; - OpeningDebit, OffsettingAmount : Decimal; + OpeningDebit: Decimal; PriorYear: Integer; begin - // [SCENARIO] Combinations that net to zero at the end date are inserted by the first pass (instead of being - // dropped as All Zero) so the second pass can produce correct debit/credit splits. Combinations with no net - // activity at all are then removed, while those with period activity are kept with correct gross columns. + // [SCENARIO] A combination that nets to zero at the end date but has period activity keeps correct debit/credit + // splits: the first pass inserts it (carrying the end-date totals) so the second pass can subtract the opening. Initialize(); PriorYear := Date2DMY(WorkDate(), 3) - 1; OpeningDebit := 1000; - OffsettingAmount := 500; // [GIVEN] An account whose opening debit is fully reversed by a credit within the period (net-zero end, period activity) CreateGLAccount(KeptAccount); CreateGLEntryWithAmount(KeptAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), OpeningDebit); CreateGLEntryWithAmount(KeptAccount."No.", '', '', '', DMY2Date(15, 6, Date2DMY(WorkDate(), 3)), -OpeningDebit); - // [GIVEN] An account with only offsetting opening turnover and no period activity (net-zero everything) - CreateGLAccount(DroppedAccount); - CreateGLEntryWithAmount(DroppedAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), OffsettingAmount); - CreateGLEntryWithAmount(DroppedAccount."No.", '', '', '', DMY2Date(15, 6, PriorYear), -OffsettingAmount); - // [WHEN] Running the query-based trial balance for the current year GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); - // [THEN] The active combination is kept with correct net AND gross debit/credit columns - TempTrialBalanceData.Reset(); + // [THEN] The combination is present with correct net AND gross debit/credit columns TempTrialBalanceData.SetRange("G/L Account No.", KeptAccount."No."); - Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist; the combination has period activity'); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the net-zero-at-end combination'); Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Starting Balance", 'Starting Balance should equal the opening debit'); Assert.AreEqual(-OpeningDebit, TempTrialBalanceData."Net Change", 'Net Change should reverse the opening'); Assert.AreEqual(0, TempTrialBalanceData.Balance, 'Balance should net to zero at the end date'); @@ -710,11 +710,6 @@ codeunit 139544 "Trial Balance Excel Reports" Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Net Change (Credit)", 'Period credit turnover equals the reversal'); Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Balance (Debit)", 'Cumulative debit at the end date'); Assert.AreEqual(OpeningDebit, TempTrialBalanceData."Balance (Credit)", 'Cumulative credit at the end date'); - - // [THEN] The inactive combination is dropped entirely (previously it surfaced as a phantom stale row) - TempTrialBalanceData.Reset(); - TempTrialBalanceData.SetRange("G/L Account No.", DroppedAccount."No."); - Assert.IsTrue(TempTrialBalanceData.IsEmpty(), 'A combination with no net activity should be dropped'); end; [Test]