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..32d6a211fb 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) @@ -250,23 +247,24 @@ 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; + // 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(); // 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); 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 + // 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; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceQuery.DimensionValue2Code; @@ -318,17 +316,16 @@ 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; + // 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(); // 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); @@ -336,6 +333,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 + // 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; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceBUQuery.DimensionValue2Code; @@ -360,57 +359,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) @@ -491,6 +541,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 b2468f3b9a..28d9313d53 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -469,6 +469,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; + TempTrialBalanceData: 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, TempTrialBalanceData); + + // [THEN] The child End-Total equals the entry amount (the posting account counted 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. + 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] procedure QueryPathPopulatesBudgetFields() var @@ -591,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 @@ -617,10 +662,54 @@ 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 : Record "G/L Account"; + TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + OpeningDebit: Decimal; + PriorYear: Integer; + begin + // [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; + + // [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); + + // [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 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 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'); + 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'); end; [Test] @@ -659,6 +748,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; + local procedure CreateSampleBusinessUnits(HowMany: Integer) var BusinessUnit: Record "Business Unit"; @@ -702,6 +832,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";