diff --git a/src/Apps/NA/MX_DIOT/app/src/Install/DIOTInitialize.Codeunit.al b/src/Apps/NA/MX_DIOT/app/src/Install/DIOTInitialize.Codeunit.al index 75424951bc..34d387ce41 100644 --- a/src/Apps/NA/MX_DIOT/app/src/Install/DIOTInitialize.Codeunit.al +++ b/src/Apps/NA/MX_DIOT/app/src/Install/DIOTInitialize.Codeunit.al @@ -17,9 +17,6 @@ codeunit 27020 "DIOT - Initialize" trigger OnInstallAppPerCompany() begin - if InitializeDone() then - exit; - InitializeCompany(); end; @@ -31,14 +28,6 @@ codeunit 27020 "DIOT - Initialize" InsertDefaultDIOTCountryData(); end; - local procedure InitializeDone(): boolean - var - AppInfo: ModuleInfo; - begin - NavApp.GetCurrentModuleInfo(AppInfo); - exit(AppInfo.DataVersion() <> Version.Create('0.0.0.0')); - end; - local procedure ApplyEvaluationClassificationsForPrivacy() var Company: Record Company; diff --git a/src/Apps/W1/Sustainability/app/src/Ledger/SustEntryReverseMgt.Codeunit.al b/src/Apps/W1/Sustainability/app/src/Ledger/SustEntryReverseMgt.Codeunit.al new file mode 100644 index 0000000000..193bf9592b --- /dev/null +++ b/src/Apps/W1/Sustainability/app/src/Ledger/SustEntryReverseMgt.Codeunit.al @@ -0,0 +1,115 @@ +namespace Microsoft.Sustainability.Ledger; + +codeunit 6243 "Sust. Entry Reverse Mgt." +{ + Permissions = tabledata "Sustainability Ledger Entry" = rimd; + + var + AlreadyReversedErr: Label 'Entry No. %1 has already been reversed.', Comment = '%1 = Entry No.'; + DocumentEntryErr: Label 'Entry No. %1 was posted from a document and cannot be reversed from here. Use a corrective document instead.', Comment = '%1 = Entry No.'; + ConfirmReverseQst: Label 'Do you want to reverse the selected sustainability ledger entry?'; + ConfirmReverseMultipleQst: Label 'Do you want to reverse %1 sustainability ledger entries?', Comment = '%1 = Count'; + + procedure ReverseEntry(var SustLedgEntry: Record "Sustainability Ledger Entry") + var + NewSustLedgEntry: Record "Sustainability Ledger Entry"; + begin + ValidateEntryForReversal(SustLedgEntry); + + CreateReversalEntry(SustLedgEntry, NewSustLedgEntry); + UpdateOriginalEntry(SustLedgEntry, NewSustLedgEntry."Entry No."); + end; + + procedure ReverseEntryFromGL(var SustLedgEntry: Record "Sustainability Ledger Entry") + var + NewSustLedgEntry: Record "Sustainability Ledger Entry"; + begin + if SustLedgEntry.Reversed then + exit; + + CreateReversalEntry(SustLedgEntry, NewSustLedgEntry); + UpdateOriginalEntry(SustLedgEntry, NewSustLedgEntry."Entry No."); + end; + + procedure ReverseEntries(var SustLedgEntry: Record "Sustainability Ledger Entry"): Integer + var + TempSustLedgEntry: Record "Sustainability Ledger Entry"; + EntryCount: Integer; + begin + EntryCount := SustLedgEntry.Count(); + + if EntryCount = 0 then + exit(0); + + if EntryCount = 1 then begin + if not Confirm(ConfirmReverseQst) then + exit(0); + end else + if not Confirm(ConfirmReverseMultipleQst, false, EntryCount) then + exit(0); + + // Validate all entries first (all-or-nothing) + TempSustLedgEntry.Copy(SustLedgEntry); + TempSustLedgEntry.SetLoadFields("Entry No.", Reversed, "Journal Template Name"); + if TempSustLedgEntry.FindSet() then + repeat + ValidateEntryForReversal(TempSustLedgEntry); + until TempSustLedgEntry.Next() = 0; + + // Reverse all entries + if SustLedgEntry.FindSet(true) then + repeat + ReverseEntry(SustLedgEntry); + until SustLedgEntry.Next() = 0; + + exit(EntryCount); + end; + + local procedure ValidateEntryForReversal(SustLedgEntry: Record "Sustainability Ledger Entry") + begin + if SustLedgEntry.Reversed then + Error(AlreadyReversedErr, SustLedgEntry."Entry No."); + + if SustLedgEntry."Journal Template Name" = '' then + Error(DocumentEntryErr, SustLedgEntry."Entry No."); + end; + + local procedure CreateReversalEntry(OriginalEntry: Record "Sustainability Ledger Entry"; var NewEntry: Record "Sustainability Ledger Entry") + begin + NewEntry.Init(); + NewEntry.TransferFields(OriginalEntry, false); + // AutoIncrement assigns the Entry No. on Insert (matches Sustainability Post Mgt and G/L reversal engine-assigned numbering). + NewEntry."Entry No." := 0; + // Post the reversal on the original entry's posting date so emissions net to zero within the same period (matches G/L Reverse). + NewEntry."Posting Date" := OriginalEntry."Posting Date"; + NewEntry."Document No." := OriginalEntry."Document No."; + NewEntry.Validate("User ID", CopyStr(UserId(), 1, MaxStrLen(NewEntry."User ID"))); + + // Negate emission values + NewEntry."Emission CO2" := -OriginalEntry."Emission CO2"; + NewEntry."Emission CH4" := -OriginalEntry."Emission CH4"; + NewEntry."Emission N2O" := -OriginalEntry."Emission N2O"; + NewEntry."CO2e Emission" := -OriginalEntry."CO2e Emission"; + NewEntry."Carbon Fee" := -OriginalEntry."Carbon Fee"; + + // Negate water & waste values + NewEntry."Water Intensity" := -OriginalEntry."Water Intensity"; + NewEntry."Discharged Into Water" := -OriginalEntry."Discharged Into Water"; + NewEntry."Waste Intensity" := -OriginalEntry."Waste Intensity"; + NewEntry."Energy Consumption" := -OriginalEntry."Energy Consumption"; + + // Set reversal tracking fields + NewEntry.Reversed := true; + NewEntry."Reversed Entry No." := OriginalEntry."Entry No."; + NewEntry."Reversed by Entry No." := 0; + + NewEntry.Insert(true); + end; + + local procedure UpdateOriginalEntry(var OriginalEntry: Record "Sustainability Ledger Entry"; ReversalEntryNo: Integer) + begin + OriginalEntry.Reversed := true; + OriginalEntry."Reversed by Entry No." := ReversalEntryNo; + OriginalEntry.Modify(true); + end; +} diff --git a/src/Apps/W1/Sustainability/app/src/Ledger/SustGLReverseSubscriber.Codeunit.al b/src/Apps/W1/Sustainability/app/src/Ledger/SustGLReverseSubscriber.Codeunit.al new file mode 100644 index 0000000000..c96e1cb09f --- /dev/null +++ b/src/Apps/W1/Sustainability/app/src/Ledger/SustGLReverseSubscriber.Codeunit.al @@ -0,0 +1,39 @@ +namespace Microsoft.Sustainability.Ledger; + +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Ledger; +using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Finance.GeneralLedger.Reversal; + +codeunit 6244 "Sust. GL Reverse Subscriber" +{ + Permissions = tabledata "Sustainability Ledger Entry" = rimd; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Reverse", 'OnReverseGLEntryOnAfterInsertGLEntry', '', false, false)] + local procedure ReverseSustLedgerEntriesOnReverseGLEntry(var GLEntry: Record "G/L Entry"; GenJnlLine: Record "Gen. Journal Line"; GLEntry2: Record "G/L Entry"; var GenJnlPostLine: Codeunit "Gen. Jnl.-Post Line") + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustLedgEntryToReverse: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + EntryNos: List of [Integer]; + EntryNo: Integer; + begin + // GLEntry2 is the original G/L entry being reversed. Sustainability Ledger Entries are linked to it + // through the shared Document No. and Posting Date (see Sust. Gen. Journal Subscriber posting logic). + // The event fires once per reversed G/L entry, so a document may be processed more than once; + // ReverseEntryFromGL skips already-reversed entries, making this idempotent. + SustLedgEntry.SetLoadFields("Entry No."); + SustLedgEntry.SetRange("Document No.", GLEntry2."Document No."); + SustLedgEntry.SetRange("Posting Date", GLEntry2."Posting Date"); + SustLedgEntry.SetRange(Reversed, false); + SustLedgEntry.SetFilter("Journal Template Name", '<>%1', ''); + if SustLedgEntry.FindSet() then + repeat + EntryNos.Add(SustLedgEntry."Entry No."); + until SustLedgEntry.Next() = 0; + + foreach EntryNo in EntryNos do + if SustLedgEntryToReverse.Get(EntryNo) then + SustEntryReverseMgt.ReverseEntryFromGL(SustLedgEntryToReverse); + end; +} diff --git a/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntries.Page.al b/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntries.Page.al index acd787bb39..8f63af43d3 100644 --- a/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntries.Page.al +++ b/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntries.Page.al @@ -161,6 +161,18 @@ page 6220 "Sustainability Ledger Entries" { ToolTip = 'Specifies the Energy Consumption.'; } + field(Reversed; Rec.Reversed) + { + ToolTip = 'Specifies whether this entry has been reversed.'; + } + field("Reversed by Entry No."; Rec."Reversed by Entry No.") + { + ToolTip = 'Specifies the entry number that reversed this entry.'; + } + field("Reversed Entry No."; Rec."Reversed Entry No.") + { + ToolTip = 'Specifies the original entry number that this entry reverses.'; + } field("Country/Region Code"; Rec."Country/Region Code") { ToolTip = 'Specifies the country/region code of the entry.'; @@ -260,6 +272,33 @@ page 6220 "Sustainability Ledger Entries" } } } + area(processing) + { + action(ReverseTransaction) + { + ApplicationArea = Basic, Suite; + Caption = 'Reverse Transaction'; + ToolTip = 'Reverse the selected sustainability ledger entry by creating a new entry with negated emission values.'; + Image = ReverseRegister; + + trigger OnAction() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + ReversedCount: Integer; + begin + CurrPage.SetSelectionFilter(SustLedgEntry); + ReversedCount := SustEntryReverseMgt.ReverseEntries(SustLedgEntry); + if ReversedCount > 0 then begin + CurrPage.Update(false); + if ReversedCount = 1 then + Message(ReversalSuccessMsg) + else + Message(ReversalMultipleSuccessMsg, ReversedCount); + end; + end; + } + } area(Promoted) { group(Category_Category4) @@ -268,6 +307,11 @@ page 6220 "Sustainability Ledger Entries" actionref(Dimensions_Promoted; Dimensions) { } actionref(SetDimensionFilter_Promoted; SetDimensionFilter) { } } + group(Category_Process) + { + Caption = 'Process'; + actionref(ReverseTransaction_Promoted; ReverseTransaction) { } + } } } @@ -275,6 +319,8 @@ page 6220 "Sustainability Ledger Entries" DimensionSetIDFilter: Page "Dimension Set ID Filter"; Dim1Visible, Dim2Visible, Dim3Visible, Dim4Visible, Dim5Visible, Dim6Visible, Dim7Visible, Dim8Visible : Boolean; DimensionCaptionLbl: Label '%1 %2', Locked = true; + ReversalSuccessMsg: Label 'The entry has been successfully reversed.'; + ReversalMultipleSuccessMsg: Label '%1 entries have been successfully reversed.', Comment = '%1 = Count'; trigger OnOpenPage() begin @@ -287,4 +333,4 @@ page 6220 "Sustainability Ledger Entries" begin DimensionManagement.UseShortcutDims(Dim1Visible, Dim2Visible, Dim3Visible, Dim4Visible, Dim5Visible, Dim6Visible, Dim7Visible, Dim8Visible); end; -} \ No newline at end of file +} diff --git a/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntry.Table.al b/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntry.Table.al index 2daa68054b..22207d1125 100644 --- a/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntry.Table.al +++ b/src/Apps/W1/Sustainability/app/src/Ledger/SustainabilityLedgerEntry.Table.al @@ -340,6 +340,26 @@ table 6216 "Sustainability Ledger Entry" { Caption = 'Correction'; } + field(5818; Reversed; Boolean) + { + Caption = 'Reversed'; + DataClassification = SystemMetadata; + Editable = false; + } + field(5819; "Reversed by Entry No."; Integer) + { + Caption = 'Reversed by Entry No.'; + DataClassification = SystemMetadata; + Editable = false; + TableRelation = "Sustainability Ledger Entry"."Entry No."; + } + field(5820; "Reversed Entry No."; Integer) + { + Caption = 'Reversed Entry No.'; + DataClassification = SystemMetadata; + Editable = false; + TableRelation = "Sustainability Ledger Entry"."Entry No."; + } } keys @@ -370,4 +390,4 @@ table 6216 "Sustainability Ledger Entry" begin DimMgt.ShowDimensionSet("Dimension Set ID", StrSubstNo(EntryRecIDLbl, TableCaption(), "Entry No.")); end; -} \ No newline at end of file +} diff --git a/src/Apps/W1/Sustainability/app/src/Permissions/SustainabilityEdit.permissionset.al b/src/Apps/W1/Sustainability/app/src/Permissions/SustainabilityEdit.permissionset.al index 9695c9b2cc..e8caa12479 100644 --- a/src/Apps/W1/Sustainability/app/src/Permissions/SustainabilityEdit.permissionset.al +++ b/src/Apps/W1/Sustainability/app/src/Permissions/SustainabilityEdit.permissionset.al @@ -14,6 +14,6 @@ permissionset 6213 "Sustainability Edit" tabledata "Sustainability Jnl. Template" = IMD, tabledata "Sustainability Jnl. Batch" = IMD, tabledata "Sustainability Jnl. Line" = IMD, - tabledata "Sustainability Ledger Entry" = I, + tabledata "Sustainability Ledger Entry" = im, tabledata "Sustainability Value Entry" = I; } \ No newline at end of file diff --git a/src/Apps/W1/Sustainability/test/src/SustGeneralJournalTest.Codeunit.al b/src/Apps/W1/Sustainability/test/src/SustGeneralJournalTest.Codeunit.al index c04911e5c1..607ebb3897 100644 --- a/src/Apps/W1/Sustainability/test/src/SustGeneralJournalTest.Codeunit.al +++ b/src/Apps/W1/Sustainability/test/src/SustGeneralJournalTest.Codeunit.al @@ -1,9 +1,12 @@ namespace Microsoft.Test.Sustainability; using Microsoft.Bank.BankAccount; +using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Finance.GeneralLedger.Posting; using Microsoft.Finance.GeneralLedger.Preview; +using Microsoft.Finance.GeneralLedger.Reversal; using Microsoft.Foundation.Navigate; using Microsoft.Purchases.Vendor; using Microsoft.Sustainability.Account; @@ -892,6 +895,77 @@ codeunit 148188 "Sust. General Journal Test" true, true, true, '', false); end; + [Test] + procedure VerifySustLedgerEntryIsReversedWhenGLTransactionIsReversed() + var + SustLedgerEntry: Record "Sustainability Ledger Entry"; + ReversalSustLedgerEntry: Record "Sustainability Ledger Entry"; + SustainabilityAccount: Record "Sustainability Account"; + GenJournalTemplate: Record "Gen. Journal Template"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + GLEntry: Record "G/L Entry"; + ReversalEntry: Record "Reversal Entry"; + GLAccount: Record "G/L Account"; + BalGLAccount: Record "G/L Account"; + EmissionCO2: Decimal; + OriginalEntryNo: Integer; + DocumentNo: Code[20]; + CategoryCode: Code[20]; + SubcategoryCode: Code[20]; + AccountCode: Code[20]; + begin + // [SCENARIO] When a G/L transaction that produced a Sustainability Ledger Entry is reversed, the sustainability entry is reversed automatically. + LibrarySustainability.CleanUpBeforeTesting(); + + // [GIVEN] A Sustainability Account and a General Journal Line posted with emissions. + CreateSustainabilityAccount(AccountCode, CategoryCode, SubcategoryCode, LibraryRandom.RandInt(10)); + SustainabilityAccount.Get(AccountCode); + EmissionCO2 := LibraryRandom.RandIntInRange(10, 20); + + // Post a plain G/L-to-G/L journal line with a blank document type so the reversal is + // localization-neutral (invoice documents cannot be reversed in some localizations, and + // bank/cash posting groups are not configured with a G/L account in every demo database). + LibraryERM.CreateGLAccount(GLAccount); + LibraryERM.CreateGLAccount(BalGLAccount); + LibraryERM.CreateGenJournalTemplate(GenJournalTemplate); + LibraryERM.CreateGenJournalBatch(GenJournalBatch, GenJournalTemplate.Name); + LibraryERM.CreateGeneralJnlLine( + GenJournalLine, GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::" ", GenJournalLine."Account Type"::"G/L Account", GLAccount."No.", + LibraryRandom.RandIntInRange(100, 200)); + GenJournalLine.Validate("Bal. Account Type", GenJournalLine."Bal. Account Type"::"G/L Account"); + GenJournalLine.Validate("Bal. Account No.", BalGLAccount."No."); + GenJournalLine.Validate("Sust. Account No.", SustainabilityAccount."No."); + GenJournalLine.Validate("Total Emission CO2", EmissionCO2); + GenJournalLine.Modify(true); + DocumentNo := GenJournalLine."Document No."; + LibraryERM.PostGeneralJnlLine(GenJournalLine); + + SustLedgerEntry.SetRange("Document No.", DocumentNo); + SustLedgerEntry.FindFirst(); + OriginalEntryNo := SustLedgerEntry."Entry No."; + + // [WHEN] The G/L transaction is reversed. + GLEntry.SetRange("Document No.", DocumentNo); + GLEntry.FindFirst(); + ReversalEntry.SetHideDialog(true); + ReversalEntry.SetHideWarningDialogs(); + ReversalEntry.ReverseTransaction(GLEntry."Transaction No."); + + // [THEN] The original sustainability entry is marked as reversed. + SustLedgerEntry.Get(OriginalEntryNo); + Assert.IsTrue(SustLedgerEntry."Reversed", 'Original sustainability entry should be reversed after G/L reversal.'); + Assert.AreNotEqual(0, SustLedgerEntry."Reversed by Entry No.", 'Reversed by Entry No. should be populated.'); + + // [THEN] A reversal sustainability entry exists with negated emission. + ReversalSustLedgerEntry.Get(SustLedgerEntry."Reversed by Entry No."); + Assert.AreEqual(-SustLedgerEntry."Emission CO2", ReversalSustLedgerEntry."Emission CO2", + 'Reversal sustainability entry should have negated CO2.'); + Assert.AreEqual(OriginalEntryNo, ReversalSustLedgerEntry."Reversed Entry No.", + 'Reversal sustainability entry should reference the original entry.'); + end; + [PageHandler] [Scope('OnPrem')] procedure GLPostingPreviewHandler(var GLPostingPreview: TestPage "G/L Posting Preview") diff --git a/src/Apps/W1/Sustainability/test/src/SustReversalTests.Codeunit.al b/src/Apps/W1/Sustainability/test/src/SustReversalTests.Codeunit.al new file mode 100644 index 0000000000..aab307b261 --- /dev/null +++ b/src/Apps/W1/Sustainability/test/src/SustReversalTests.Codeunit.al @@ -0,0 +1,407 @@ +codeunit 148222 "Sust. Reversal Tests" +{ + Subtype = Test; + TestPermissions = Disabled; + + trigger OnRun() + begin + // [FEATURE] [Sustainability] [Reverse Transaction] + end; + + var + Assert: Codeunit Assert; + IsInitialized: Boolean; + + [Test] + procedure ReverseSimpleSustEntry() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + OriginalEntryNo: Integer; + OriginalCO2: Decimal; + begin + // [SCENARIO] Reverse a simple sustainability journal entry and verify emission values are negated + // [GIVEN] A posted Sustainability Ledger Entry from a journal + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + OriginalEntryNo := SustLedgEntry."Entry No."; + OriginalCO2 := SustLedgEntry."Emission CO2"; + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The original entry is marked as reversed + SustLedgEntry.Get(OriginalEntryNo); + Assert.IsTrue(SustLedgEntry."Reversed", 'Original entry should be marked as Reversed.'); + Assert.AreNotEqual(0, SustLedgEntry."Reversed by Entry No.", 'Reversed by Entry No. should be populated.'); + + // [THEN] A new reversal entry exists with negated values + VerifyReversalEntry(SustLedgEntry."Reversed by Entry No.", OriginalEntryNo, -OriginalCO2); + end; + + [Test] + procedure ReverseEntryNegatesAllEmissionFields() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + ReversalEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] All emission fields (CO2, CH4, N2O, CO2e, Carbon Fee, Water, Waste, Energy) are negated + // [GIVEN] A sustainability entry with all emission fields populated + Initialize(); + CreateSustLedgerEntryWithAllFields(SustLedgEntry); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The reversal entry has all emission fields negated + SustLedgEntry.Get(SustLedgEntry."Entry No."); + ReversalEntry.Get(SustLedgEntry."Reversed by Entry No."); + + Assert.AreEqual(-SustLedgEntry."Emission CO2", ReversalEntry."Emission CO2", 'CO2 should be negated but values changed'); + Assert.AreEqual(-SustLedgEntry."Emission CH4", ReversalEntry."Emission CH4", 'CH4 should be negated but values changed'); + Assert.AreEqual(-SustLedgEntry."Emission N2O", ReversalEntry."Emission N2O", 'N2O should be negated but values changed'); + Assert.AreEqual(-SustLedgEntry."CO2e Emission", ReversalEntry."CO2e Emission", 'CO2e should be negated but values changed'); + Assert.AreEqual(-SustLedgEntry."Carbon Fee", ReversalEntry."Carbon Fee", 'Carbon Fee should be negated but values changed'); + Assert.AreEqual(-SustLedgEntry."Water Intensity", ReversalEntry."Water Intensity", 'Water Intensity should be negated'); + Assert.AreEqual(-SustLedgEntry."Discharged Into Water", ReversalEntry."Discharged Into Water", 'Discharged Into Water should be negated'); + Assert.AreEqual(-SustLedgEntry."Waste Intensity", ReversalEntry."Waste Intensity", 'Waste Intensity should be negated'); + Assert.AreEqual(-SustLedgEntry."Energy Consumption", ReversalEntry."Energy Consumption", 'Energy Consumption should be negated'); + end; + + [Test] + procedure ReverseAlreadyReversedEntryThrowsError() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] Attempting to reverse an already-reversed entry should throw an error + // [GIVEN] A sustainability entry that is already reversed + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + SustLedgEntry."Reversed" := true; + SustLedgEntry."Reversed by Entry No." := SustLedgEntry."Entry No." + 1; + SustLedgEntry.Modify(); + + // [WHEN] The user tries to reverse it again + // [THEN] An error is thrown + asserterror SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + Assert.ExpectedError('has already been reversed'); + end; + + [Test] + procedure ReverseDocumentPostedEntryThrowsError() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] Entries posted from documents (Journal Template Name = '') cannot be reversed + // [GIVEN] A sustainability entry posted from a purchase document (empty Journal Template Name) + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, '', ''); + + // [WHEN] The user tries to reverse it + // [THEN] An error is thrown telling to use corrective document + asserterror SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + Assert.ExpectedError('posted from a document'); + end; + + [Test] + procedure ReverseGeneralJournalEntryIsAllowed() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] Entries posted from General Journal (Template Name = 'GENERAL') can be reversed + // [GIVEN] A sustainability entry posted from General Journal + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'GENERAL', 'DEFAULT'); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The entry is successfully reversed + SustLedgEntry.Get(SustLedgEntry."Entry No."); + Assert.IsTrue(SustLedgEntry."Reversed", 'General Journal entry should be reversible.'); + end; + + [Test] + procedure ReversalEntryIsAlsoMarkedReversed() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + ReversalEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] The reversal entry itself should be marked as Reversed (so it cannot be reversed again) + // [GIVEN] A posted sustainability entry + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The reversal entry is also marked as reversed + SustLedgEntry.Get(SustLedgEntry."Entry No."); + ReversalEntry.Get(SustLedgEntry."Reversed by Entry No."); + Assert.IsTrue(ReversalEntry."Reversed", 'Reversal entry should be marked as Reversed.'); + Assert.AreEqual(SustLedgEntry."Entry No.", ReversalEntry."Reversed Entry No.", + 'Reversal entry should point back to original entry.'); + end; + + [Test] + procedure ReversalPreservesDocumentNo() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + ReversalEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] The reversal entry should keep the same Document No. as the original + // [GIVEN] A posted sustainability entry with a Document No. + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The reversal entry has the same Document No. + SustLedgEntry.Get(SustLedgEntry."Entry No."); + ReversalEntry.Get(SustLedgEntry."Reversed by Entry No."); + Assert.AreEqual(SustLedgEntry."Document No.", ReversalEntry."Document No.", + 'Reversal entry should have same Document No. as original.'); + end; + + [Test] + procedure ReversalPreservesPostingDate() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + ReversalEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + OriginalPostingDate: Date; + begin + // [SCENARIO] The reversal entry should post on the original entry's posting date (matches G/L Reverse), not WorkDate + // [GIVEN] A posted sustainability entry with a posting date different from WorkDate + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + OriginalPostingDate := CalcDate('<-1M>', WorkDate()); + SustLedgEntry."Posting Date" := OriginalPostingDate; + SustLedgEntry.Modify(); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The reversal entry has the same Posting Date as the original + SustLedgEntry.Get(SustLedgEntry."Entry No."); + ReversalEntry.Get(SustLedgEntry."Reversed by Entry No."); + Assert.AreEqual(OriginalPostingDate, ReversalEntry."Posting Date", + 'Reversal entry should post on the original entry''s posting date.'); + end; + + [Test] + procedure ReversalStampsUserId() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + ReversalEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] The reversal entry should stamp the current User ID (matches Sustainability posting and G/L Reverse) + // [GIVEN] A posted sustainability entry + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + + // [WHEN] The entry is reversed + SustEntryReverseMgt.ReverseEntry(SustLedgEntry); + + // [THEN] The reversal entry has the current User ID stamped + SustLedgEntry.Get(SustLedgEntry."Entry No."); + ReversalEntry.Get(SustLedgEntry."Reversed by Entry No."); + Assert.AreEqual(CopyStr(UserId(), 1, MaxStrLen(ReversalEntry."User ID")), ReversalEntry."User ID", + 'Reversal entry should stamp the current User ID.'); + end; + + [Test] + [HandlerFunctions('ConfirmYesHandler')] + procedure ReverseMultipleEntriesConfirmYes() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustLedgEntry2: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + SelectionFilter: Record "Sustainability Ledger Entry"; + begin + // [SCENARIO] Reversing multiple selected entries when user confirms Yes + // [GIVEN] Two sustainability entries from journal + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + CreateSustLedgerEntry(SustLedgEntry2, 'SUSTJNL', 'DEFAULT'); + + // [WHEN] Both entries are selected and reversed with confirmation + SelectionFilter.SetFilter("Entry No.", '%1|%2', SustLedgEntry."Entry No.", SustLedgEntry2."Entry No."); + SustEntryReverseMgt.ReverseEntries(SelectionFilter); + + // [THEN] Both entries are reversed + SustLedgEntry.Get(SustLedgEntry."Entry No."); + SustLedgEntry2.Get(SustLedgEntry2."Entry No."); + Assert.IsTrue(SustLedgEntry."Reversed", 'First entry should be reversed.'); + Assert.IsTrue(SustLedgEntry2."Reversed", 'Second entry should be reversed.'); + end; + + [Test] + [HandlerFunctions('ConfirmNoHandler')] + procedure ReverseMultipleEntriesConfirmNo() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + SelectionFilter: Record "Sustainability Ledger Entry"; + begin + // [SCENARIO] Reversing entries when user declines confirmation should leave entries unchanged + // [GIVEN] A sustainability entry from journal + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + + // [WHEN] Reversal is attempted but user says No + SelectionFilter.SetFilter("Entry No.", '%1', SustLedgEntry."Entry No."); + SustEntryReverseMgt.ReverseEntries(SelectionFilter); + + // [THEN] Entry is not reversed + SustLedgEntry.Get(SustLedgEntry."Entry No."); + Assert.IsFalse(SustLedgEntry."Reversed", 'Entry should not be reversed when user declines.'); + end; + + [Test] + [HandlerFunctions('ConfirmYesHandler')] + procedure ReverseMultipleWithOneAlreadyReversedThrowsError() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustLedgEntry2: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + SelectionFilter: Record "Sustainability Ledger Entry"; + begin + // [SCENARIO] If any entry in the selection is already reversed, all-or-nothing validation should fail + // [GIVEN] Two entries - one already reversed, one not + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'SUSTJNL', 'DEFAULT'); + CreateSustLedgerEntry(SustLedgEntry2, 'SUSTJNL', 'DEFAULT'); + SustLedgEntry2."Reversed" := true; + SustLedgEntry2.Modify(); + + // [WHEN] Both are selected for reversal + SelectionFilter.SetFilter("Entry No.", '%1|%2', SustLedgEntry."Entry No.", SustLedgEntry2."Entry No."); + + // [THEN] Error is thrown (all-or-nothing validation fails on the reversed entry) + asserterror SustEntryReverseMgt.ReverseEntries(SelectionFilter); + Assert.ExpectedError('has already been reversed'); + end; + + [Test] + procedure ReverseFromGLSkipsAlreadyReversed() + var + SustLedgEntry: Record "Sustainability Ledger Entry"; + SustEntryReverseMgt: Codeunit "Sust. Entry Reverse Mgt."; + begin + // [SCENARIO] ReverseEntryFromGL should silently skip already-reversed entries + // [GIVEN] An already-reversed sustainability entry + Initialize(); + CreateSustLedgerEntry(SustLedgEntry, 'GENERAL', 'DEFAULT'); + SustLedgEntry."Reversed" := true; + SustLedgEntry.Modify(); + + // [WHEN] ReverseEntryFromGL is called (from G/L reversal hook) + SustEntryReverseMgt.ReverseEntryFromGL(SustLedgEntry); + + // [THEN] No error - it silently exits without creating a reversal + SustLedgEntry.Get(SustLedgEntry."Entry No."); + Assert.AreEqual(0, SustLedgEntry."Reversed by Entry No.", + 'Already-reversed entry should not get a new reversal link.'); + end; + + // --- Helper Procedures --- + + local procedure Initialize() + begin + if IsInitialized then + exit; + IsInitialized := true; + end; + + local procedure CreateSustLedgerEntry(var SustLedgEntry: Record "Sustainability Ledger Entry"; JournalTemplateName: Code[10]; BatchName: Code[10]) + var + NextEntryNo: Integer; + begin + SustLedgEntry.SetCurrentKey("Entry No."); + if SustLedgEntry.FindLast() then + NextEntryNo := SustLedgEntry."Entry No." + 1 + else + NextEntryNo := 1; + + SustLedgEntry.Init(); + SustLedgEntry."Entry No." := NextEntryNo; + SustLedgEntry."Posting Date" := WorkDate(); + SustLedgEntry."Document No." := CopyStr(Format(CreateGuid()), 1, 20); + SustLedgEntry."Journal Template Name" := JournalTemplateName; + SustLedgEntry."Journal Batch Name" := BatchName; + SustLedgEntry."Emission CO2" := 42.5; + SustLedgEntry."Emission CH4" := 3.2; + SustLedgEntry."Emission N2O" := 1.1; + SustLedgEntry."CO2e Emission" := 42.5 + 3.2 * 25 + 1.1 * 298; + SustLedgEntry."Carbon Fee" := 15.75; + SustLedgEntry.Insert(false); + end; + + local procedure CreateSustLedgerEntryWithAllFields(var SustLedgEntry: Record "Sustainability Ledger Entry") + var + NextEntryNo: Integer; + begin + SustLedgEntry.SetCurrentKey("Entry No."); + if SustLedgEntry.FindLast() then + NextEntryNo := SustLedgEntry."Entry No." + 1 + else + NextEntryNo := 1; + + SustLedgEntry.Init(); + SustLedgEntry."Entry No." := NextEntryNo; + SustLedgEntry."Posting Date" := WorkDate(); + SustLedgEntry."Document No." := CopyStr(Format(CreateGuid()), 1, 20); + SustLedgEntry."Journal Template Name" := 'SUSTJNL'; + SustLedgEntry."Journal Batch Name" := 'DEFAULT'; + SustLedgEntry."Emission CO2" := 55.0; + SustLedgEntry."Emission CH4" := 7.3; + SustLedgEntry."Emission N2O" := 2.8; + SustLedgEntry."CO2e Emission" := 120.0; + SustLedgEntry."Carbon Fee" := 25.5; + SustLedgEntry."Water Intensity" := 30.0; + SustLedgEntry."Discharged Into Water" := 18.5; + SustLedgEntry."Waste Intensity" := 12.0; + SustLedgEntry."Energy Consumption" := 450.0; + SustLedgEntry.Insert(false); + end; + + local procedure VerifyReversalEntry(ReversalEntryNo: Integer; OriginalEntryNo: Integer; ExpectedCO2: Decimal) + var + ReversalEntry: Record "Sustainability Ledger Entry"; + begin + ReversalEntry.Get(ReversalEntryNo); + Assert.AreEqual(ExpectedCO2, ReversalEntry."Emission CO2", 'CO2 should be negated.'); + Assert.IsTrue(ReversalEntry."Reversed", 'Reversal entry should be marked as Reversed.'); + Assert.AreEqual(OriginalEntryNo, ReversalEntry."Reversed Entry No.", + 'Reversal entry should reference original entry.'); + end; + + // --- Handler Functions --- + + [ConfirmHandler] + procedure ConfirmYesHandler(Question: Text[1024]; var Reply: Boolean) + begin + Reply := true; + end; + + [ConfirmHandler] + procedure ConfirmNoHandler(Question: Text[1024]; var Reply: Boolean) + begin + Reply := false; + end; + + [MessageHandler] + procedure MessageHandler(Message: Text[1024]) + begin + // Consume the success message + end; +}