diff --git a/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/GenerateSAFT13File.Codeunit.al b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/GenerateSAFT13File.Codeunit.al index 721530724b2..52be5e10da1 100644 --- a/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/GenerateSAFT13File.Codeunit.al +++ b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/GenerateSAFT13File.Codeunit.al @@ -607,6 +607,39 @@ codeunit 10692 "Generate SAF-T 1.3 File" until SAFTSourceCode.Next() = 0; end; + internal procedure CalculateDebitCreditTotals(var SAFTExportHeader: Record "SAF-T Export Header") + var + SAFTSourceCode: Record "SAF-T Source Code"; + TempSourceCode: Record "Source Code" temporary; + SAFTGLEntryTotal: Query "SAF-T G/L Entry Total"; + SourceCodeFilter: Text; + begin + SAFTExportHeader."Total G/L Entry Debit" := 0; + SAFTExportHeader."Total G/L Entry Credit" := 0; + if not SAFTSourceCode.FindSet() then + SAFTSourceCode.Init(); + repeat + PopulateSourceCodeBuffer(TempSourceCode, SAFTSourceCode); + SourceCodeFilter := GetSourceCodeFilter(TempSourceCode); + if SourceCodeFilter <> '' then begin + Clear(SAFTGLEntryTotal); + SAFTGLEntryTotal.SetRange(PostingDate, SAFTExportHeader."Starting Date", SAFTExportHeader."Ending Date"); + SAFTGLEntryTotal.SetFilter(SourceCode, SourceCodeFilter); + SAFTGLEntryTotal.SetFilter(AmountFilter, '>%1', 0); + SAFTGLEntryTotal.Open(); + if SAFTGLEntryTotal.Read() then + SAFTExportHeader."Total G/L Entry Debit" += SAFTGLEntryTotal.TotalAmount; + SAFTGLEntryTotal.Close(); + + SAFTGLEntryTotal.SetFilter(AmountFilter, '<%1', 0); + SAFTGLEntryTotal.Open(); + if SAFTGLEntryTotal.Read() then + SAFTExportHeader."Total G/L Entry Credit" -= SAFTGLEntryTotal.TotalAmount; + SAFTGLEntryTotal.Close(); + end; + until SAFTSourceCode.Next() = 0; + end; + local procedure PopulateSourceCodeBuffer(var TempSourceCode: Record "Source Code" temporary; var SAFTSourceCode: Record "SAF-T Source Code") var SourceCode: Record "Source Code"; diff --git a/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTExportMgt.Codeunit.al b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTExportMgt.Codeunit.al index 42102fa819c..c6eb65ad44c 100644 --- a/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTExportMgt.Codeunit.al +++ b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTExportMgt.Codeunit.al @@ -86,16 +86,18 @@ codeunit 10675 "SAF-T Export Mgt." GenerateSAFT13File: Codeunit "Generate SAF-T 1.3 File"; SAFTGLEntryByTrans: Query "SAF-T G/L Entry By Trans."; begin - GLEntry.SetCurrentKey("Transaction No."); - GLEntry.SetRange("Posting Date", SAFTExportHeader."Starting Date", SAFTExportHeader."Ending Date"); - GLEntry.CalcSums("Debit Amount", "Credit Amount"); - SAFTExportHeader."Total G/L Entry Debit" := GLEntry."Debit Amount"; - SAFTExportHeader."Total G/L Entry Credit" := GLEntry."Credit Amount"; SAFTExportHeader."Number of G/L Entries" := 0; - if SAFTExportHeader.Version = SAFTExportHeader.Version::"1.30" then - SAFTExportHeader."Number of G/L Entries" := GenerateSAFT13File.CountTransactions(SAFTExportHeader) + if SAFTExportHeader.Version = SAFTExportHeader.Version::"1.30" then begin + GenerateSAFT13File.CalculateDebitCreditTotals(SAFTExportHeader); + SAFTExportHeader."Number of G/L Entries" := GenerateSAFT13File.CountTransactions(SAFTExportHeader); + end else begin + GLEntry.SetCurrentKey("Transaction No."); + GLEntry.SetRange("Posting Date", SAFTExportHeader."Starting Date", SAFTExportHeader."Ending Date"); + GLEntry.CalcSums("Debit Amount", "Credit Amount"); + SAFTExportHeader."Total G/L Entry Debit" := GLEntry."Debit Amount"; + SAFTExportHeader."Total G/L Entry Credit" := GLEntry."Credit Amount"; SAFTGLEntryByTrans.SetRange(Posting_Date, SAFTExportHeader."Starting Date", SAFTExportHeader."Ending Date"); if SAFTGLEntryByTrans.Open() then begin while SAFTGLEntryByTrans.Read() do diff --git a/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTGLEntryTotal.Query.al b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTGLEntryTotal.Query.al new file mode 100644 index 00000000000..30e76ab2ff0 --- /dev/null +++ b/src/Apps/NO/NorwegianSAFT/app/src/Export Engine/SAFTGLEntryTotal.Query.al @@ -0,0 +1,32 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Finance.AuditFileExport; + +using Microsoft.Finance.GeneralLedger.Ledger; + +query 10672 "SAF-T G/L Entry Total" +{ + DataAccessIntent = ReadOnly; + + elements + { + dataitem(GLEntry; "G/L Entry") + { + filter(PostingDate; "Posting Date") + { + } + filter(SourceCode; "Source Code") + { + } + filter(AmountFilter; Amount) + { + } + column(TotalAmount; Amount) + { + Method = Sum; + } + } + } +} diff --git a/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests.Codeunit.al b/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests.Codeunit.al index e6729cdc87e..b6353ffb685 100644 --- a/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests.Codeunit.al +++ b/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests.Codeunit.al @@ -1788,6 +1788,10 @@ codeunit 148103 "SAF-T XML Tests" TempXMLBuffer.Next(); VerifyChildElementsCount(TempChildXMLBuffer, TempXMLBuffer, 1); SAFTTestHelper.AssertCurrentElementValue(TempChildXMLBuffer, 'n1:Amount', SAFTTestHelper.FormatAmount(PaymentAmount)); + + // [THEN] SAF-T 1.20 retains its signed debit/credit column totals after reversal + SAFTTestHelper.AssertCurrentValue(TempXMLBuffer, '/n1:AuditFile/n1:GeneralLedgerEntries/n1:TotalDebit', '0'); + SAFTTestHelper.AssertCurrentValue(TempXMLBuffer, '/n1:AuditFile/n1:GeneralLedgerEntries/n1:TotalCredit', '0'); end; [Test] diff --git a/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests13.Codeunit.al b/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests13.Codeunit.al index 2de5a453576..f8034321581 100644 --- a/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests13.Codeunit.al +++ b/src/Apps/NO/NorwegianSAFT/test/src/SAFTXMLTests13.Codeunit.al @@ -1693,7 +1693,7 @@ codeunit 148110 "SAF-T XML Tests 1.3" Amount[2] := LibraryRandom.RandDec(100, 2); SAFTTestHelper.MockGLEntry( SAFTExportHeader."Ending Date", LibraryUtility.GenerateGUID(), '', - LastUsedTransactionNo + 2, 0, 0, '', '', 0, '', '', 0, Amount[2]); + LastUsedTransactionNo + 2, 0, 0, '', '', 0, '', '', 0, -Amount[2]); // [WHEN] Export G/L Entries to the XML file LibraryVariableStorage.Enqueue(GenerateSAFTFileImmediatelyQst); @@ -1979,6 +1979,9 @@ codeunit 148110 "SAF-T XML Tests 1.3" TempXMLBuffer.Next(); VerifyChildElementsCount(TempChildXMLBuffer, TempXMLBuffer, 1); SAFTTestHelper.AssertCurrentElementValue(TempChildXMLBuffer, 'n1:Amount', SAFTTestHelper.FormatAmount(PaymentAmount)); + + // [THEN] Reversals contribute positive amounts on the opposite XML side, including in the control totals + VerifySelectionAmountTotals(SAFTExportHeader.ID, 1, 2 * PaymentAmount, 2 * PaymentAmount); end; [Test] @@ -2058,6 +2061,7 @@ codeunit 148110 "SAF-T XML Tests 1.3" VerifyChildElementsCount(TempChildXMLBuffer, TempXMLBuffer, 1); SAFTTestHelper.AssertCurrentElementValue( TempChildXMLBuffer, 'n1:Amount', SAFTTestHelper.FormatAmount(Abs(GenJournalLine."Amount (LCY)"))); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 1, Abs(GenJournalLine."Amount (LCY)"), Abs(GenJournalLine."Amount (LCY)")); LibraryVariableStorage.AssertEmpty(); end; @@ -2388,6 +2392,7 @@ codeunit 148110 "SAF-T XML Tests 1.3" // [THEN] The fallback journal preserves all groups and lines Assert.ExpectedMessage(GenerateSAFTFileImmediatelyQst, LibraryVariableStorage.DequeueText()); VerifySelectionTransactionCount(SAFTExportHeader, 1, 3, 4); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 1, 400, 0); LibraryVariableStorage.AssertEmpty(); end; @@ -2412,6 +2417,7 @@ codeunit 148110 "SAF-T XML Tests 1.3" // [THEN] The generated file contains no transactions or lines and reports zero Assert.ExpectedMessage(GenerateSAFTFileImmediatelyQst, LibraryVariableStorage.DequeueText()); VerifySelectionTransactionCount(SAFTExportHeader, 1, 0, 0); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 1, 0, 0); LibraryVariableStorage.AssertEmpty(); end; @@ -2488,6 +2494,7 @@ codeunit 148110 "SAF-T XML Tests 1.3" // [THEN] Both G/L files retain exact header values, positive credit magnitudes and metadata, and no excluded analysis VerifySelectionTransactionCount(SAFTExportHeader, 2, 3, 3); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 2, 100, 200); SAFTExportLine.SetRange("Master Data", false); SAFTTestHelper.FindSAFTExportLine(SAFTExportLine, SAFTExportHeader.ID); repeat @@ -2521,6 +2528,87 @@ codeunit 148110 "SAF-T XML Tests 1.3" LibraryVariableStorage.AssertEmpty(); end; + [Test] + [HandlerFunctions('CaptureConfirmationHandler')] + procedure AmountTotalsIncludeNegativeDebitAndCreditEntries() + var + SAFTExportHeader: Record "SAF-T Export Header"; + GLAccount: Record "G/L Account"; + SourceCode: Record "Source Code"; + begin + // [FEATURE] [AI test 1.0] [Totals] + // [SCENARIO] Negative debit and credit entries contribute to the opposite XML side across journals. + Initialize(); + + // [GIVEN] Journals "J1", "J2" and "J3" contain ordinary and correction entries, including a blank source code + CreateTransactionCountSetup(SAFTExportHeader, GLAccount, SourceCode); + CreateAmountTotalsEntries(SAFTExportHeader, GLAccount."No.", SourceCode); + + // [WHEN] Export a single transaction file + SAFTTestHelper.RunSAFTExport(SAFTExportHeader); + + // [THEN] The control totals equal the debit and credit amounts in every journal + Assert.ExpectedMessage(GenerateSAFTFileImmediatelyQst, LibraryVariableStorage.DequeueText()); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 1, 120, 110); + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('CaptureConfirmationHandler')] + procedure AmountTotalsRepeatAcrossMonthlyFiles() + var + SAFTExportHeader: Record "SAF-T Export Header"; + GLAccount: Record "G/L Account"; + SourceCode: Record "Source Code"; + begin + // [FEATURE] [AI test 1.0] [Totals] + // [SCENARIO] Each monthly file repeats the debit and credit sums of all transaction files in the selection. + Initialize(); + + // [GIVEN] Ordinary and correction entries in two months, plus entries outside the selection + CreateTransactionCountSetup(SAFTExportHeader, GLAccount, SourceCode); + SAFTExportHeader.Validate("Ending Date", CalcDate('<1M+CM>', SAFTExportHeader."Starting Date")); + SAFTExportHeader.Validate("Split By Month", true); + SAFTExportHeader.Modify(true); + CreateAmountTotalsEntries(SAFTExportHeader, GLAccount."No.", SourceCode); + + // [WHEN] Export monthly transaction files + SAFTTestHelper.RunSAFTExport(SAFTExportHeader); + + // [THEN] Both files repeat the entire selection totals without including outside-period entries + Assert.ExpectedMessage(GenerateSAFTFileImmediatelyQst, LibraryVariableStorage.DequeueText()); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 2, 120, 110); + LibraryVariableStorage.AssertEmpty(); + end; + + [Test] + [HandlerFunctions('CaptureConfirmationHandler')] + procedure AmountTotalsRepeatAcrossDailyFiles() + var + SAFTExportHeader: Record "SAF-T Export Header"; + GLAccount: Record "G/L Account"; + SourceCode: Record "Source Code"; + begin + // [FEATURE] [AI test 1.0] [Totals] + // [SCENARIO] Each daily file repeats the debit and credit sums of the complete selection. + Initialize(); + + // [GIVEN] Ordinary and correction entries on two days separated by an empty day + CreateTransactionCountSetup(SAFTExportHeader, GLAccount, SourceCode); + SAFTExportHeader.Validate("Ending Date", SAFTExportHeader."Starting Date" + 2); + SAFTExportHeader.Validate("Split By Date", true); + SAFTExportHeader.Modify(true); + CreateAmountTotalsEntries(SAFTExportHeader, GLAccount."No.", SourceCode); + + // [WHEN] Export daily transaction files + SAFTTestHelper.RunSAFTExport(SAFTExportHeader); + + // [THEN] Both files repeat the entire selection totals, not just their own line amounts + Assert.ExpectedMessage(GenerateSAFTFileImmediatelyQst, LibraryVariableStorage.DequeueText()); + VerifySelectionAmountTotals(SAFTExportHeader.ID, 2, 120, 110); + LibraryVariableStorage.AssertEmpty(); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(CODEUNIT::"SAF-T XML Tests 1.3"); @@ -2678,6 +2766,42 @@ codeunit 148110 "SAF-T XML Tests 1.3" CashReceiptJournal.Post.Invoke(); end; + local procedure CreateAmountTotalsEntries(SAFTExportHeader: Record "SAF-T Export Header"; GLAccountNo: Code[20]; SourceCode: Record "Source Code") + var + SAFTSourceCode: Record "SAF-T Source Code"; + SecondSourceCode: Record "Source Code"; + begin + SAFTSourceCode.ModifyAll("Includes No Source Code", false); + CreateTransactionCountJournal(SAFTSourceCode, false); + LibraryERM.CreateSourceCode(SecondSourceCode); + SecondSourceCode.Validate("SAF-T Source Code", SAFTSourceCode.Code); + SecondSourceCode.Modify(true); + CreateTransactionCountJournal(SAFTSourceCode, true); + CreateTransactionCountJournal(SAFTSourceCode, false); + + CreateAmountTotalsEntry(SAFTExportHeader."Starting Date", GLAccountNo, SourceCode.Code, 100, false); + CreateAmountTotalsEntry(SAFTExportHeader."Starting Date", GLAccountNo, SourceCode.Code, -70, false); + CreateAmountTotalsEntry(SAFTExportHeader."Ending Date", GLAccountNo, SecondSourceCode.Code, -40, true); + CreateAmountTotalsEntry(SAFTExportHeader."Ending Date", GLAccountNo, '', 20, true); + CreateAmountTotalsEntry(SAFTExportHeader."Ending Date", GLAccountNo, SourceCode.Code, 0, false); + CreateAmountTotalsEntry(SAFTExportHeader."Starting Date" - 1, GLAccountNo, SourceCode.Code, 900, false); + CreateAmountTotalsEntry(SAFTExportHeader."Ending Date" + 1, GLAccountNo, SecondSourceCode.Code, -800, false); + end; + + local procedure CreateAmountTotalsEntry(PostingDate: Date; GLAccountNo: Code[20]; SourceCode: Code[10]; Amount: Decimal; IsCorrection: Boolean) + var + GLEntry: Record "G/L Entry"; + begin + GLEntry.Get( + SAFTTestHelper.MockGLEntry( + PostingDate, LibraryUtility.GenerateGUID(), GLAccountNo, GetLastUsedTransactionNo() + 1, + 0, '', '', 0, '', SourceCode, Amount, 0)); + GLEntry.UpdateDebitCredit(IsCorrection); +#pragma warning disable AA0214 + GLEntry.Modify(true); +#pragma warning restore AA0214 + end; + local procedure LoadAnalysisExportFiles(SAFTExportHeader: Record "SAF-T Export Header"; var TempMasterXMLBuffer: Record "XML Buffer" temporary; var TempGLEntryXMLBuffer: Record "XML Buffer" temporary) var SAFTExportLine: Record "SAF-T Export Line"; @@ -3490,6 +3614,51 @@ codeunit 148110 "SAF-T XML Tests 1.3" VerifyCurrencyAmountInfo(TempChildXMLBuffer, CurrencyCode, Amount, AmountLCY, ExchangeRate); end; + local procedure VerifySelectionAmountTotals(ExportID: Integer; ExpectedFiles: Integer; ExpectedDebit: Decimal; ExpectedCredit: Decimal) + var + SAFTExportLine: Record "SAF-T Export Line"; + TempXMLBuffer: Record "XML Buffer" temporary; + TotalDebit: Decimal; + TotalCredit: Decimal; + begin +#pragma warning disable AA0210 + SAFTExportLine.SetRange("Master Data", false); +#pragma warning restore AA0210 + SAFTTestHelper.FindSAFTExportLine(SAFTExportLine, ExportID); + Assert.RecordCount(SAFTExportLine, ExpectedFiles); + repeat + SAFTTestHelper.LoadXMLBufferFromSAFTExportLine(TempXMLBuffer, SAFTExportLine); + TotalDebit += SumXMLLineAmounts(TempXMLBuffer, 'DebitAmount'); + TotalCredit += SumXMLLineAmounts(TempXMLBuffer, 'CreditAmount'); + until SAFTExportLine.Next() = 0; + Assert.AreEqual(ExpectedDebit, TotalDebit, 'Unexpected sum of exported debit amounts.'); + Assert.AreEqual(ExpectedCredit, TotalCredit, 'Unexpected sum of exported credit amounts.'); + + SAFTExportLine.FindSet(); + repeat + SAFTTestHelper.LoadXMLBufferFromSAFTExportLine(TempXMLBuffer, SAFTExportLine); + SAFTTestHelper.AssertCurrentValue( + TempXMLBuffer, '/n1:AuditFile/n1:GeneralLedgerEntries/n1:TotalDebit', SAFTTestHelper.FormatAmount(TotalDebit)); + SAFTTestHelper.AssertCurrentValue( + TempXMLBuffer, '/n1:AuditFile/n1:GeneralLedgerEntries/n1:TotalCredit', SAFTTestHelper.FormatAmount(TotalCredit)); + until SAFTExportLine.Next() = 0; + end; + + local procedure SumXMLLineAmounts(var TempXMLBuffer: Record "XML Buffer" temporary; AmountNode: Text) TotalAmount: Decimal + var + Amount: Decimal; + begin + TempXMLBuffer.Reset(); + TempXMLBuffer.FindNodesByXPath( + TempXMLBuffer, '/n1:AuditFile/n1:GeneralLedgerEntries/n1:Journal/n1:Transaction/n1:Line/n1:' + AmountNode + '/n1:Amount'); + if TempXMLBuffer.FindSet() then + repeat + Evaluate(Amount, TempXMLBuffer.Value, 9); + TotalAmount += Amount; + until TempXMLBuffer.Next() = 0; + TempXMLBuffer.Reset(); + end; + local procedure CreateAndPostPaymentJnl( var GenJournalLine: array[3] of Record "Gen. Journal Line"; SAFTExportHeader: Record "SAF-T Export Header")