diff --git a/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentManagementFR.Codeunit.al b/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentManagementFR.Codeunit.al index b32b5f13ce4..87ed5c05792 100644 --- a/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentManagementFR.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentManagementFR.Codeunit.al @@ -359,11 +359,11 @@ codeunit 10837 "Payment Management FR" SetAccountNo(); InvPostingBuffer[1]."System-Created Entry" := true; if StepLedger.Sign = StepLedger.Sign::Debit then begin - InvPostingBuffer[1].Validate(Amount, Abs(PaymentLine.Amount)); - InvPostingBuffer[1].Validate("Amount (LCY)", Abs(PaymentLine."Amount (LCY)")); + InvPostingBuffer[1].Validate(Amount, GetSettlementAmount(PaymentLine.Amount)); + InvPostingBuffer[1].Validate("Amount (LCY)", GetSettlementAmount(PaymentLine."Amount (LCY)")); end else begin - InvPostingBuffer[1].Validate(Amount, Abs(PaymentLine.Amount) * -1); - InvPostingBuffer[1].Validate("Amount (LCY)", Abs(PaymentLine."Amount (LCY)") * -1); + InvPostingBuffer[1].Validate(Amount, GetSettlementAmount(PaymentLine.Amount) * -1); + InvPostingBuffer[1].Validate("Amount (LCY)", GetSettlementAmount(PaymentLine."Amount (LCY)") * -1); end; InvPostingBuffer[1]."Currency Code" := PaymentLine."Currency Code"; InvPostingBuffer[1]."Currency Factor" := PaymentLine."Currency Factor"; @@ -413,6 +413,8 @@ codeunit 10837 "Payment Management FR" InvPostingBuffer[1]."Source No." := PaymentLine."Account No."; InvPostingBuffer[1]."External Document No." := PaymentLine."External Document No."; InvPostingBuffer[1]."Dimension Set ID" := PaymentLine."Dimension Set ID"; + NormalizeAccountLevelSign(); + NetCustomerSettlement(); OnGenerInvPostingBufferOnBeforeUpdtBuffer(InvPostingBuffer, PaymentLine, StepLedger); UpdtBuffer(); if (InvPostingBuffer[1].Amount >= 0) xor InvPostingBuffer[1].Correction then @@ -432,6 +434,74 @@ codeunit 10837 "Payment Management FR" OnAfterGetDescriptionForInvPostingBuffer(StepLedger, PaymentLine, Description); end; + local procedure GetSettlementAmount(PaymentLineAmount: Decimal): Decimal + begin + if PaymentLine."Applies-to Doc. Type" = PaymentLine."Applies-to Doc. Type"::"Credit Memo" then + exit(-Abs(PaymentLineAmount)); + exit(Abs(PaymentLineAmount)); + end; + + local procedure NormalizeAccountLevelSign() + begin + if StepLedger."Detail Level" <> StepLedger."Detail Level"::Account then + exit; + if StepLedger.Sign = StepLedger.Sign::Debit then + InvPostingBuffer[1].Sign := InvPostingBuffer[1].Sign::Positive + else + InvPostingBuffer[1].Sign := InvPostingBuffer[1].Sign::Negative; + end; + + local procedure NetCustomerSettlement() + var + SharedInvoiceLine: Record "Payment Line FR"; + CreditMemoAmount: Decimal; + begin + if InvPostingBuffer[1]."Account Type" <> InvPostingBuffer[1]."Account Type"::Customer then + exit; + if not (PaymentLine."Applies-to Doc. Type" in + [PaymentLine."Applies-to Doc. Type"::Invoice, PaymentLine."Applies-to Doc. Type"::"Credit Memo"]) + then + exit; + + if not PaymentLine.TryGetCustomerNettingContext(SharedInvoiceLine, CreditMemoAmount) then begin + if (PaymentLine."Applies-to Doc. Type" = PaymentLine."Applies-to Doc. Type"::"Credit Memo") and + (InvPostingBuffer[1]."Document Type" = InvPostingBuffer[1]."Document Type"::Payment) + then + InvPostingBuffer[1]."Document Type" := InvPostingBuffer[1]."Document Type"::Refund; + exit; + end; + + InvPostingBuffer[1]."Applies-to ID" := SharedInvoiceLine."Applies-to ID"; + InvPostingBuffer[1]."Due Date" := SharedInvoiceLine."Due Date"; + if StepLedger."Detail Level" = StepLedger."Detail Level"::Line then + InvPostingBuffer[1]."Payment Line No." := SharedInvoiceLine."Line No."; + if StepLedger."Document No." <> StepLedger."Document No."::"Header No." then + InvPostingBuffer[1]."Document No." := SharedInvoiceLine."Document No."; + InvPostingBuffer[1].Sign := InvPostingBuffer[1].Sign::Negative; + + if (PaymentLine."Applies-to Doc. Type" = PaymentLine."Applies-to Doc. Type"::Invoice) and + (PaymentLine."Line No." = SharedInvoiceLine."Line No.") + then + exit; + ApplyCreditMemoToSharedInvoice(PaymentLine."Applies-to Doc. Type", PaymentLine."Applies-to Doc. No.", SharedInvoiceLine."Applies-to ID"); + end; + + local procedure ApplyCreditMemoToSharedInvoice(DocumentType: Enum "Gen. Journal Document Type"; DocumentNo: Code[20]; AppliesToID: Code[50]) + var + DocumentCustLedgEntry: Record "Cust. Ledger Entry"; + begin + DocumentCustLedgEntry.SetRange("Customer No.", PaymentLine."Account No."); + DocumentCustLedgEntry.SetRange("Document Type", DocumentType); + DocumentCustLedgEntry.SetRange("Document No.", DocumentNo); + DocumentCustLedgEntry.SetRange(Open, true); + if not DocumentCustLedgEntry.FindFirst() then + exit; + DocumentCustLedgEntry."Applies-to ID" := AppliesToID; + DocumentCustLedgEntry.CalcFields("Remaining Amount"); + DocumentCustLedgEntry.Validate("Amount to Apply", DocumentCustLedgEntry."Remaining Amount"); + DocumentCustLedgEntry.Modify(); + end; + procedure SetPostingGroup() var PostingGroup: Code[20]; diff --git a/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentMgtSubscribersFR.Codeunit.al b/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentMgtSubscribersFR.Codeunit.al index 71e6ace39cb..34f51423db0 100644 --- a/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentMgtSubscribersFR.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/app/src/Codeunits/PaymentMgtSubscribersFR.Codeunit.al @@ -118,7 +118,7 @@ codeunit 10838 "PaymentMgt Subscribers FR" ToDirectDebitCollectionEntry.Validate("Applies-to Entry No.", AppliesToEntryNo); ToDirectDebitCollectionEntry."Transfer Date" := PaymentHeader."Posting Date"; ToDirectDebitCollectionEntry."Currency Code" := PaymentLine."Currency Code"; - ToDirectDebitCollectionEntry.Validate("Transfer Amount", PaymentLine."Credit Amount"); + ToDirectDebitCollectionEntry.Validate("Transfer Amount", GetNetTransferAmount(PaymentLine)); ToDirectDebitCollectionEntry.Validate("Mandate ID", PaymentLine."Direct Debit Mandate ID"); OnCreateTempCollectionEntriesOnBeforeInsert(ToDirectDebitCollectionEntry, PaymentHeader, PaymentLine); ToDirectDebitCollectionEntry.Insert(); @@ -319,10 +319,12 @@ codeunit 10838 "PaymentMgt Subscribers FR" var SEPADirectDebitMandate: Record "SEPA Direct Debit Mandate"; CustLedgerEntry: Record "Cust. Ledger Entry"; + SharedInvoiceLine: Record "Payment Line FR"; SummarizeNotAllowedErr: Label 'You cannot export a SEPA customer payment that is applied to multiple documents. Make sure that the Summarize per field in the Suggest Customer Payments window is blank.'; UnappliedLinesNotAllowedErr: Label 'Payment slip line %1 must be applied to a customer invoice.', Comment = '%1 = No.'; AccTypeErr: Label 'Only customer transactions are allowed.'; BankAccErr: Label 'You must use customer bank account, %1, which you specified in the selected direct debit mandate.', Comment = '%1 = code'; + CreditMemoAmount: Decimal; IsHandled: Boolean; begin IsHandled := false; @@ -340,18 +342,45 @@ codeunit 10838 "PaymentMgt Subscribers FR" if (PaymentLine."Applies-to Doc. No." = '') and (PaymentLine."Applies-to ID" = '') then DirectDebitCollectionEntry.InsertPaymentFileError(StrSubstNo(UnappliedLinesNotAllowedErr, PaymentLine."Line No.")) else begin + if (PaymentLine."Applies-to Doc. Type" = PaymentLine."Applies-to Doc. Type"::"Credit Memo") and + PaymentLine.TryGetCustomerNettingContext(SharedInvoiceLine, CreditMemoAmount) + then + exit(false); PaymentLine.GetAppliesToDocCustLedgEntry(CustLedgerEntry); - if CustLedgerEntry.Count > 1 then - DirectDebitCollectionEntry.InsertPaymentFileError(SummarizeNotAllowedErr); - CustLedgerEntry.FindFirst(); - if CustLedgerEntry."Document Type" <> CustLedgerEntry."Document Type"::Invoice then - DirectDebitCollectionEntry.InsertPaymentFileError(StrSubstNo(UnappliedLinesNotAllowedErr, PaymentLine."Line No.")); - AppliesToEntryNo := CustLedgerEntry."Entry No."; + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + case CustLedgerEntry.Count of + 0: + DirectDebitCollectionEntry.InsertPaymentFileError(StrSubstNo(UnappliedLinesNotAllowedErr, PaymentLine."Line No.")); + 1: + begin + CustLedgerEntry.FindFirst(); + AppliesToEntryNo := CustLedgerEntry."Entry No."; + end; + else + DirectDebitCollectionEntry.InsertPaymentFileError(SummarizeNotAllowedErr); + end; end; exit(not DirectDebitCollectionEntry.HasPaymentFileErrors()); end; + local procedure GetNetTransferAmount(PaymentLine: Record "Payment Line FR") TransferAmount: Decimal + var + SharedInvoiceLine: Record "Payment Line FR"; + CreditMemoAmount: Decimal; + begin + TransferAmount := PaymentLine."Credit Amount"; + + if PaymentLine."Applies-to Doc. Type" <> PaymentLine."Applies-to Doc. Type"::Invoice then + exit; + + if not PaymentLine.TryGetCustomerNettingContext(SharedInvoiceLine, CreditMemoAmount) then + exit; + + if SharedInvoiceLine."Line No." = PaymentLine."Line No." then + TransferAmount += CreditMemoAmount; + end; + local procedure PostDelayedUnrealizedVAT(GenJnlLine: Record "Gen. Journal Line") var OldCustLedgEntry: Record "Cust. Ledger Entry"; diff --git a/src/Apps/FR/PaymentManagementFR/app/src/Tables/PaymentLineFR.Table.al b/src/Apps/FR/PaymentManagementFR/app/src/Tables/PaymentLineFR.Table.al index 94171e7e47d..6e70c37aeb6 100644 --- a/src/Apps/FR/PaymentManagementFR/app/src/Tables/PaymentLineFR.Table.al +++ b/src/Apps/FR/PaymentManagementFR/app/src/Tables/PaymentLineFR.Table.al @@ -663,6 +663,45 @@ table 10837 "Payment Line FR" end; end; + internal procedure TryGetCustomerNettingContext(var SharedInvoiceLine: Record "Payment Line FR"; var CreditMemoAmount: Decimal): Boolean + var + CustomerLine: Record "Payment Line FR"; + HasCreditMemo: Boolean; + FoundInvoice: Boolean; + InvoiceCount: Integer; + begin + Clear(SharedInvoiceLine); + CreditMemoAmount := 0; + + if Rec."Applies-to ID" = '' then + exit(false); + + CustomerLine.SetRange("No.", Rec."No."); + CustomerLine.SetRange("Account Type", CustomerLine."Account Type"::Customer); + CustomerLine.SetRange("Account No.", Rec."Account No."); + CustomerLine.SetRange("Applies-to ID", Rec."Applies-to ID"); + if not CustomerLine.FindSet() then + exit(false); + repeat + case CustomerLine."Applies-to Doc. Type" of + CustomerLine."Applies-to Doc. Type"::"Credit Memo": + begin + HasCreditMemo := true; + CreditMemoAmount += CustomerLine."Credit Amount" - CustomerLine."Debit Amount"; + end; + CustomerLine."Applies-to Doc. Type"::Invoice: + begin + InvoiceCount += 1; + if (not FoundInvoice) then begin + SharedInvoiceLine := CustomerLine; + FoundInvoice := true; + end; + end; + end; + until CustomerLine.Next() = 0; + exit(HasCreditMemo and (InvoiceCount = 1)); + end; + procedure GetCurrency() var Header: Record "Payment Header FR"; diff --git a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMFRFeatureBugs.Codeunit.al b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMFRFeatureBugs.Codeunit.al index ab84c1eb427..a465dae0734 100644 --- a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMFRFeatureBugs.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMFRFeatureBugs.Codeunit.al @@ -226,11 +226,4 @@ codeunit 144008 "ERM FR Feature Bugs" PaymentClassList.OK().Invoke(); end; -#if not CLEAN28 - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Payment Management Feature FR", OnAfterCheckFeatureEnabled, '', false, false)] - local procedure OnAfterCheckFeatureEnabled(var IsEnabled: Boolean) - begin - IsEnabled := true; - end; -#endif } diff --git a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMPaymentManagement.Codeunit.al b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMPaymentManagement.Codeunit.al index 65a845b72fc..4aceb8d57c4 100644 --- a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMPaymentManagement.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/ERMPaymentManagement.Codeunit.al @@ -1922,6 +1922,61 @@ codeunit 144013 "ERM Payment Management" PaymentSlipSubform.Close(); end; + [Test] + [HandlerFunctions('PaymentClassListModalPageHandler,SuggestCustomerPaymentsSummarizedRequestPageHandler,ConfirmHandlerTrue')] + procedure PostPaymentSlipNetsCreditMemoAgainstInvoice() + var + PaymentClass: Record "Payment Class FR"; + PaymentHeader: Record "Payment Header FR"; + PaymentLine: Record "Payment Line FR"; + GenJournalLine: Record "Gen. Journal Line"; + PaymentStepLedger: Record "Payment Step Ledger FR"; + SummarizePer: Option " ",Customer,"Due date"; + CustomerNo: Code[20]; + InvoiceAmount: Decimal; + CreditMemoAmount: Decimal; + NetAmount: Decimal; + begin + // [FEATURE] [AI test] + // [SCENARIO 644993] A payment slip suggested for a customer with an open invoice and an unapplied credit memo posts a single net payment; the credit memo is applied to the invoice and no refund entry is created. + Initialize(); + + // [GIVEN] Customer "C" with an open posted invoice of 2062.80 and an open unapplied posted credit memo of 229.20. + InvoiceAmount := 2062.8; + CreditMemoAmount := 229.2; + NetAmount := InvoiceAmount - CreditMemoAmount; + CustomerNo := CreateCustomer(''); + CreateAndPostGeneralJournal( + GenJournalLine, GenJournalLine."Account Type"::Customer, CustomerNo, + GenJournalLine."Document Type"::Invoice, InvoiceAmount, WorkDate()); + CreateAndPostGeneralJournal( + GenJournalLine, GenJournalLine."Account Type"::Customer, CustomerNo, + GenJournalLine."Document Type"::"Credit Memo", -CreditMemoAmount, WorkDate()); + + // [GIVEN] A postable payment slip whose payment class posts to the ledger, with the customer's entries suggested (Summarize per Customer). + PaymentClass.Get(SetupForPaymentSlipPost(PaymentStepLedger."Detail Level"::Account, PaymentClass.Suggestions::Customer)); + CreatePaymentHeader(PaymentHeader); + Commit(); // Required for execute report. + SuggestCustomerPaymentLinesSummarized(CustomerNo, SummarizePer::Customer, PaymentHeader."No."); + + // [THEN] A single net payment line of 1833.60 is created. + PaymentLine.SetRange("No.", PaymentHeader."No."); + Assert.RecordCount(PaymentLine, 1); + PaymentHeader.CalcFields("Amount (LCY)"); + Assert.AreEqual(NetAmount, Abs(PaymentHeader."Amount (LCY)"), UnexpectedErr); + + // [WHEN] The payment slip is posted. + PostPaymentSlipHeaderNo(PaymentHeader."No."); + + // [THEN] The credit memo is applied to the invoice and a single net payment of 1833.60 closes it with no refund entry. + VerifyCustomerEntriesNettedAndClosed(CustomerNo, NetAmount); + + // [THEN] The bank G/L account is debited 1833.60 and the receivables G/L account is credited 1833.60. + VerifyBankAccountLedgerEntry(PaymentHeader, 1); + VerifyGenLedgerEntry(PaymentHeader, 1); + VerifyReceivablesGLCreditEntry(CustomerNo, PaymentHeader."No.", NetAmount); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(CODEUNIT::"ERM Payment Management"); @@ -2840,6 +2895,16 @@ codeunit 144013 "ERM Payment Management" SuggestCustomerPayments.RunModal(); end; + local procedure SuggestCustomerPaymentLinesSummarized(CustomerNo: Code[20]; SummarizePer: Option; PaymentHeaderNo: Code[20]) + var + PaymentSlip: TestPage "Payment Slip FR"; + begin + OpenPaymentSlip(PaymentSlip, PaymentHeaderNo); + LibraryVariableStorage.Enqueue(CustomerNo); + LibraryVariableStorage.Enqueue(SummarizePer); + PaymentSlip.SuggestCustomerPayments.Invoke(); // Page closes on scope exit. + end; + local procedure SuggestVendorPaymentLines(Value: Variant; Value2: Variant; PaymentHeader: Record "Payment Header FR") var SuggestVendorPaymentsFR: Report "Suggest Vend. Payments"; @@ -2901,6 +2966,47 @@ codeunit 144013 "ERM Payment Management" Assert.AreEqual(GLEntry.Count, NoOfRecord, UnexpectedErr); end; + local procedure VerifyReceivablesGLCreditEntry(CustomerNo: Code[20]; PaymentHeaderNo: Code[20]; ExpectedCreditAmount: Decimal) + var + Customer: Record Customer; + CustomerPostingGroup: Record "Customer Posting Group"; + GLEntry: Record "G/L Entry"; + begin + Customer.Get(CustomerNo); + CustomerPostingGroup.Get(Customer."Customer Posting Group"); + GLEntry.SetRange("Document No.", PaymentHeaderNo); + GLEntry.SetRange("G/L Account No.", CustomerPostingGroup."Receivables Account"); + Assert.RecordCount(GLEntry, 1); + GLEntry.FindFirst(); + GLEntry.TestField("Credit Amount", ExpectedCreditAmount); + GLEntry.TestField("Debit Amount", 0); + end; + + local procedure VerifyCustomerEntriesNettedAndClosed(CustomerNo: Code[20]; NetAmount: Decimal) + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetRange("Customer No.", CustomerNo); + + // No refund entry is created for the credit memo. + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Refund); + Assert.RecordCount(CustLedgerEntry, 0); + + // A single net payment settles the balance. + CustLedgerEntry.SetFilter( + "Document Type", '<>%1&<>%2', + CustLedgerEntry."Document Type"::Invoice, CustLedgerEntry."Document Type"::"Credit Memo"); + Assert.RecordCount(CustLedgerEntry, 1); + CustLedgerEntry.FindFirst(); + CustLedgerEntry.CalcFields(Amount); + Assert.AreEqual(-NetAmount, CustLedgerEntry.Amount, UnexpectedErr); + + // The invoice, credit memo and net payment are all fully applied. + CustLedgerEntry.SetRange("Document Type"); + CustLedgerEntry.SetRange(Open, true); + Assert.RecordCount(CustLedgerEntry, 0); + end; + local procedure ClearPaymentSlipData() var PaymentClass: Record "Payment Class FR"; diff --git a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/SEPA02DDFunctionalTest.Codeunit.al b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/SEPA02DDFunctionalTest.Codeunit.al index 9ce6a689402..426f79245f2 100644 --- a/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/SEPA02DDFunctionalTest.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/test/src/RegularTests/SEPA02DDFunctionalTest.Codeunit.al @@ -34,7 +34,11 @@ codeunit 144022 "SEPA.02 DD Functional Test" LibraryUtility: Codeunit "Library - Utility"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; LibraryRandom: Codeunit "Library - Random"; - LibraryXMLRead: Codeunit "Library - XML Read"; + LibraryXMLRead: Codeunit "Library - XML Read OnServer"; +#if not CLEAN28 + PaymentMgtFeatureSubscriber: Codeunit "SEPA.02 DD Functional Test"; +#endif + IsInitialized: Boolean; SEPA_PartnerType: Option ,Company,Person; UnexpectedEmptyNodeErr: Label 'Unexpected empty value for node <%1> of subtree <%2>.', Comment = '%1 = Node Name, %2 = Subtree Root Name'; OneToManyNotAllowedErr: Label 'You cannot export a SEPA customer payment that is applied to multiple documents.'; @@ -521,6 +525,142 @@ codeunit 144022 "SEPA.02 DD Functional Test" VerifySEPAMandate(PaymentLine."Direct Debit Mandate ID", 1); end; + [Test] + [HandlerFunctions('PaymentClassHandler,ConfirmHandler,SuggestCustPaymentsSummarizedReqPageHandler')] + procedure ExportSEPAFileWithInvoiceAndCreditMemoForSameCustomer() + var + Customer: Record Customer; + PaymentHeader: Record "Payment Header FR"; + PaymentLine: Record "Payment Line FR"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + CreditMemoCustLedgerEntry: Record "Cust. Ledger Entry"; + NetAmount: Decimal; + SEPAFilePath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 644993] Suggesting a Person customer's payments with summarization nets an invoice and an unapplied credit memo into a single line; the SEPA file collects the net balance. + + // [GIVEN] A Person customer with a posted invoice and a posted unapplied credit memo (half the invoice) sharing the same direct debit mandate. + CreateCustomerWithInvoice(Customer, InvoiceCustLedgerEntry, SEPA_PartnerType::Person); + InvoiceCustLedgerEntry.CalcFields("Remaining Amount"); + CreateCustomerCreditMemoLedgerEntry( + CreditMemoCustLedgerEntry, Customer."No.", InvoiceCustLedgerEntry."Direct Debit Mandate ID", + Round(InvoiceCustLedgerEntry."Remaining Amount" / 2, 0.01)); + CreditMemoCustLedgerEntry.CalcFields("Remaining Amount"); + NetAmount := InvoiceCustLedgerEntry."Remaining Amount" + CreditMemoCustLedgerEntry."Remaining Amount"; + + // [GIVEN] A SEPA direct debit payment slip for the Person partner type. + CreatePaymentHeader(PaymentHeader, SEPA_PartnerType::Person); + + // [WHEN] Customer payments are suggested with Summarize per Customer. + SuggestCustomerPaymentsSummarized(PaymentHeader, Customer."No."); + + // [THEN] A single net payment line is created for the customer's balance. + PaymentLine.SetRange("No.", PaymentHeader."No."); + Assert.RecordCount(PaymentLine, 1); + PaymentLine.FindFirst(); + Assert.AreEqual(NetAmount, PaymentLine."Credit Amount", 'Suggested payment should collect the net customer balance.'); + + // [GIVEN] The summarized line is assigned the customer's direct debit mandate. + PaymentLine.Validate("Direct Debit Mandate ID", InvoiceCustLedgerEntry."Direct Debit Mandate ID"); + PaymentLine.Modify(true); + + // [WHEN] The SEPA direct debit file is exported. + SEPAFilePath := ExportSEPAFile(PaymentHeader); + Commit(); + LibraryXMLRead.Initialize(SEPAFilePath); + + // [THEN] A single net transaction is exported and the mandate is debited once. + LibraryXMLRead.VerifyNodeValueInSubtree('GrpHdr', 'NbOfTxs', '1'); + LibraryXMLRead.VerifyNodeValueInSubtree('GrpHdr', 'CtrlSum', Format(PaymentLine."Credit Amount", 0, 9)); + LibraryXMLRead.VerifyNodeValueInSubtree('DrctDbtTxInf', 'InstdAmt', PaymentLine."Credit Amount"); + VerifySEPAMandate(InvoiceCustLedgerEntry."Direct Debit Mandate ID", 1); + + // Clean data + PaymentHeader.Delete(true); + end; + + [Test] + [HandlerFunctions('PaymentClassHandler,ConfirmHandler')] + procedure ExportPmtLineWithCreditMemoIsNettedWithoutError() + var + Customer: Record Customer; + PaymentHeader: Record "Payment Header FR"; + PaymentLine: Record "Payment Line FR"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + CreditMemoCustLedgerEntry: Record "Cust. Ledger Entry"; + SEPAFilePath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 644993] A credit memo collected with an invoice is netted into the payment and no longer raises the "must be applied to a customer invoice" error. + + // [GIVEN] A Person customer with a posted invoice and a posted credit memo (half the invoice). + CreateCustomerWithInvoice(Customer, InvoiceCustLedgerEntry, SEPA_PartnerType::Person); + InvoiceCustLedgerEntry.CalcFields("Remaining Amount"); + CreateCustomerCreditMemoLedgerEntry( + CreditMemoCustLedgerEntry, Customer."No.", InvoiceCustLedgerEntry."Direct Debit Mandate ID", + Round(InvoiceCustLedgerEntry."Remaining Amount" / 2, 0.01)); + + // [GIVEN] A single payment slip line collecting the net balance, with the invoice and the credit memo applied to it. + CreatePaymentSlip(PaymentHeader, PaymentLine, Customer."No.", InvoiceCustLedgerEntry."Direct Debit Mandate ID", SEPA_PartnerType::Person); + ApplyInvoiceAndCreditMemoToPaymentLine(PaymentLine, InvoiceCustLedgerEntry, CreditMemoCustLedgerEntry); + + // [WHEN] The SEPA direct debit file is exported. + SEPAFilePath := ExportSEPAFile(PaymentHeader); + Commit(); + LibraryXMLRead.Initialize(SEPAFilePath); + + // [THEN] The export succeeds collecting the net amount, with no payment file error for the line. + Assert.AreNotEqual('', SEPAFilePath, 'SEPA file should be generated.'); + LibraryXMLRead.VerifyNodeValueInSubtree('DrctDbtTxInf', 'InstdAmt', PaymentLine."Credit Amount"); + VerifyPaymentErrors( + DATABASE::"Payment Header FR", PaymentHeader."No.", PaymentLine."Line No.", + StrSubstNo(UnappliedLinesNotAllowedErr, PaymentLine."Line No."), 0); + + // Clean data + PaymentHeader.Delete(true); + end; + + [Test] + [HandlerFunctions('PaymentClassHandler,ConfirmHandler')] + procedure ExportInvoiceAppliedByIDWithCreditMemoSucceeds() + var + Customer: Record Customer; + PaymentHeader: Record "Payment Header FR"; + PaymentLine: Record "Payment Line FR"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + CreditMemoCustLedgerEntry: Record "Cust. Ledger Entry"; + SEPAFilePath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 644993] When an invoice and a credit memo of the same customer share the Applies-to ID, the SEPA export collects the net balance and succeeds without the "multiple documents" error. + + // [GIVEN] A Person customer with a posted invoice and a posted credit memo (half the invoice). + CreateCustomerWithInvoice(Customer, InvoiceCustLedgerEntry, SEPA_PartnerType::Person); + InvoiceCustLedgerEntry.CalcFields("Remaining Amount"); + CreateCustomerCreditMemoLedgerEntry( + CreditMemoCustLedgerEntry, Customer."No.", InvoiceCustLedgerEntry."Direct Debit Mandate ID", + Round(InvoiceCustLedgerEntry."Remaining Amount" / 2, 0.01)); + + // [GIVEN] A payment slip line collecting the net balance, with the invoice and the credit memo applied by a shared Applies-to ID. + CreatePaymentSlip(PaymentHeader, PaymentLine, Customer."No.", InvoiceCustLedgerEntry."Direct Debit Mandate ID", SEPA_PartnerType::Person); + ApplyInvoiceAndCreditMemoToPaymentLine(PaymentLine, InvoiceCustLedgerEntry, CreditMemoCustLedgerEntry); + + // [WHEN] The SEPA direct debit file is exported. + SEPAFilePath := ExportSEPAFile(PaymentHeader); + Commit(); + LibraryXMLRead.Initialize(SEPAFilePath); + + // [THEN] The export collects the net amount and succeeds without the "multiple documents" error, and the mandate is debited once. + Assert.AreNotEqual('', SEPAFilePath, 'SEPA file should be generated.'); + LibraryXMLRead.VerifyNodeValueInSubtree('DrctDbtTxInf', 'InstdAmt', PaymentLine."Credit Amount"); + VerifyPaymentErrors(DATABASE::"Payment Header FR", PaymentHeader."No.", PaymentLine."Line No.", OneToManyNotAllowedErr, 0); + VerifySEPAMandate(InvoiceCustLedgerEntry."Direct Debit Mandate ID", 1); + + // Clean data + PaymentHeader.Delete(true); + end; + local procedure CreateSEPABankAccount(var BankAccount: Record "Bank Account") begin LibraryERM.CreateBankAccount(BankAccount); @@ -591,6 +731,17 @@ codeunit 144022 "SEPA.02 DD Functional Test" PaymentHeader[2].Delete(true); end; + local procedure Initialize() + begin + if IsInitialized then + exit; + IsInitialized := true; +#if not CLEAN28 + // Bind the manual subscription so "Payment Management Feature FR".IsEnabled() is forced on, independent of the feature key state. + BindSubscription(PaymentMgtFeatureSubscriber); +#endif + end; + local procedure CreatePaymentClass(): Text[30] var PaymentClass: Record "Payment Class FR"; @@ -619,6 +770,7 @@ codeunit 144022 "SEPA.02 DD Functional Test" BankAccount: Record "Bank Account"; PaymentClassCode: Code[30]; begin + Initialize(); PaymentClassCode := CreatePaymentClass(); LibraryVariableStorage.Enqueue(PaymentClassCode); LibraryFRLocalization.CreatePaymentHeader(PaymentHeader); @@ -641,6 +793,18 @@ codeunit 144022 "SEPA.02 DD Functional Test" PaymentLine.Modify(true); end; + local procedure SuggestCustomerPaymentsSummarized(var PaymentHeader: Record "Payment Header FR"; CustomerNo: Code[20]) + var + Customer: Record Customer; + SuggestCustomerPayments: Report "Suggest Cust. Payments"; + begin + Commit(); // Required for running the report modally. + SuggestCustomerPayments.SetGenPayLine(PaymentHeader); + Customer.SetRange("No.", CustomerNo); + SuggestCustomerPayments.SetTableView(Customer); + SuggestCustomerPayments.RunModal(); + end; + local procedure CreateCustomerWithInvoice(var Customer: Record Customer; var CustLedgerEntry: Record "Cust. Ledger Entry"; SEPAPartnerType: Option) var CustomerBankAccount: Record "Customer Bank Account"; @@ -701,6 +865,40 @@ codeunit 144022 "SEPA.02 DD Functional Test" CustLedgerEntry.FindLast(); end; + local procedure CreateCustomerCreditMemoLedgerEntry(var CustLedgerEntry: Record "Cust. Ledger Entry"; CustomerNo: Code[20]; SEPADirectDebitMandateID: Code[35]; CreditMemoAmount: Decimal) + var + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + begin + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLine(GenJournalLine, GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::"Credit Memo", GenJournalLine."Account Type"::Customer, + CustomerNo, -CreditMemoAmount); + GenJournalLine."Direct Debit Mandate ID" := SEPADirectDebitMandateID; + GenJournalLine."Payment Method Code" := ''; + GenJournalLine.Modify(); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + CustLedgerEntry.SetRange("Customer No.", GenJournalLine."Account No."); + CustLedgerEntry.SetRange("Document No.", GenJournalLine."Document No."); + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::"Credit Memo"); + CustLedgerEntry.FindLast(); + end; + + local procedure ApplyInvoiceAndCreditMemoToPaymentLine(var PaymentLine: Record "Payment Line FR"; var InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; var CreditMemoCustLedgerEntry: Record "Cust. Ledger Entry") + begin + PaymentLine.Validate("Applies-to ID", PaymentLine."Document No."); + InvoiceCustLedgerEntry.CalcFields("Remaining Amount"); + CreditMemoCustLedgerEntry.CalcFields("Remaining Amount"); + // Collect the customer's net balance: invoice remaining plus the negative credit memo remaining. + PaymentLine.Validate("Credit Amount", InvoiceCustLedgerEntry."Remaining Amount" + CreditMemoCustLedgerEntry."Remaining Amount"); + PaymentLine.Modify(true); + InvoiceCustLedgerEntry."Applies-to ID" := PaymentLine."Document No."; + InvoiceCustLedgerEntry.Modify(); + CreditMemoCustLedgerEntry."Applies-to ID" := PaymentLine."Document No."; + CreditMemoCustLedgerEntry.Modify(); + end; + local procedure CreateCustomerAddress(var Customer: Record Customer) begin Customer.Validate(Address, LibraryUtility.GenerateRandomCode(Customer.FieldNo(Address), DATABASE::Customer)); @@ -940,6 +1138,16 @@ codeunit 144022 "SEPA.02 DD Functional Test" SuggestCustomerPayments.OK().Invoke(); end; + [RequestPageHandler] + procedure SuggestCustPaymentsSummarizedReqPageHandler(var SuggestCustomerPayments: TestRequestPage "Suggest Cust. Payments") + var + SummarizePer: Option " ",Customer,"Due date"; + begin + SuggestCustomerPayments.LastPaymentDate.SetValue(WorkDate()); + SuggestCustomerPayments.Summarize_Per.SetValue(SummarizePer::Customer); + SuggestCustomerPayments.OK().Invoke(); + end; + #if not CLEAN28 [EventSubscriber(ObjectType::Codeunit, Codeunit::"Payment Management Feature FR", OnAfterCheckFeatureEnabled, '', false, false)] local procedure OnAfterCheckFeatureEnabled(var IsEnabled: Boolean) diff --git a/src/Apps/FR/PaymentManagementFR/test/src/TestLibraries/LibraryLocalizationFR.Codeunit.al b/src/Apps/FR/PaymentManagementFR/test/src/TestLibraries/LibraryLocalizationFR.Codeunit.al index 59ce9ea520d..11253c80798 100644 --- a/src/Apps/FR/PaymentManagementFR/test/src/TestLibraries/LibraryLocalizationFR.Codeunit.al +++ b/src/Apps/FR/PaymentManagementFR/test/src/TestLibraries/LibraryLocalizationFR.Codeunit.al @@ -90,5 +90,13 @@ codeunit 144043 "Library - Localization FR" begin CODEUNIT.Run(CODEUNIT::"Payment Management FR"); end; + +#if not CLEAN28 + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Payment Management Feature FR", OnAfterCheckFeatureEnabled, '', false, false)] + local procedure OnAfterCheckFeatureEnabled(var IsEnabled: Boolean) + begin + IsEnabled := true; + end; +#endif }