From 08c3cc01ea826c66b4d8d8d2ca39d3044db046e9 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 11:19:17 +0200 Subject: [PATCH 01/49] Add synchronous E-Document message transport --- .../EDocCoreObjects.PermissionSet.al | 2 + .../Interfaces/IMessageSender.Interface.al | 13 ++++ .../Integration/ServiceIntegration.Enum.al | 6 +- .../Message/EDocMessageContext.Codeunit.al | 62 +++++++++++++++++++ .../Message/EDocMessageMgt.Codeunit.al | 39 ++++++++++++ .../EDocMessageTransportDefault.Codeunit.al | 23 +++++++ .../Message/EDocumentMessageAPI.Codeunit.al | 28 +++++++++ 7 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index 782f758045d..dc5fa50a5c5 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -104,7 +104,9 @@ permissionset 6100 "E-Doc. Core - Objects" #endif codeunit "E-Doc. Attachment Processor" = X, codeunit "E-Doc. Hist. Line Data Loader" = X, + codeunit "E-Doc. Message Context" = X, codeunit "E-Doc. Message Mgt." = X, + codeunit "E-Doc. Msg. Transport Default" = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, page "E-Doc. Changes Preview" = X, diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al new file mode 100644 index 00000000000..0d58c12091a --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Integration.Interfaces; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; + +interface IMessageSender +{ + procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"); +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al index 6a91bb64230..3049d5902d7 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al @@ -6,12 +6,14 @@ namespace Microsoft.eServices.EDocument.Integration; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; -enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, IConsentManager +enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, IConsentManager, IMessageSender { Extensible = true; Access = Public; - DefaultImplementation = IConsentManager = "Consent Manager Default Impl."; + DefaultImplementation = IConsentManager = "Consent Manager Default Impl.", + IMessageSender = "E-Doc. Msg. Transport Default"; value(0; "No Integration") { diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al new file mode 100644 index 00000000000..3d052c68d72 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al @@ -0,0 +1,62 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument.Integration; +using Microsoft.eServices.EDocument.Integration.Action; +using System.Utilities; + +codeunit 6533 "E-Doc. Message Context" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + internal procedure Initialize(EDocMessage: Record "E-Document Message"; TempBlob: Codeunit "Temp Blob") + begin + MessageEntryNo := EDocMessage."Entry No."; + MessageType := EDocMessage."Message Type"; + ResponseType := EDocMessage."Response Type"; + Payload := TempBlob; + end; + + procedure GetMessageEntryNo(): Integer + begin + exit(MessageEntryNo); + end; + + procedure GetMessageType(): Enum "E-Document Message Type" + begin + exit(MessageType); + end; + + procedure GetResponseType(): Enum "E-Doc. Response Type" + begin + exit(ResponseType); + end; + + procedure GetTempBlob(): Codeunit "Temp Blob" + begin + exit(Payload); + end; + + procedure Http(): Codeunit "Http Message State" + begin + exit(HttpMessageState); + end; + + procedure Status(): Codeunit "Integration Action Status" + begin + exit(IntegrationActionStatus); + end; + + var + HttpMessageState: Codeunit "Http Message State"; + IntegrationActionStatus: Codeunit "Integration Action Status"; + Payload: Codeunit "Temp Blob"; + MessageType: Enum "E-Document Message Type"; + ResponseType: Enum "E-Doc. Response Type"; + MessageEntryNo: Integer; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index b11299cce5f..915431b9d44 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Processing.Message; using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration.Interfaces; using System.Utilities; /// @@ -79,6 +80,40 @@ codeunit 6433 "E-Doc. Message Mgt." TempBlob := EDocDataStorage.GetTempBlob(); end; + procedure SendMessage(MessageEntryNo: Integer) + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + EDocMessage: Record "E-Document Message"; + EDocMessageContext: Codeunit "E-Doc. Message Context"; + EDocumentLog: Codeunit "E-Document Log"; + TempBlob: Codeunit "Temp Blob"; + MessageSender: Interface IMessageSender; + begin + EDocMessage.Get(MessageEntryNo); + EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); + EDocMessage.TestField(Status, EDocMessage.Status::Created); + EDocMessage.TestField(Service); + + EDocument.Get(EDocMessage."E-Document Entry No."); + EDocumentService.Get(EDocMessage.Service); + GetMessageBlob(MessageEntryNo, TempBlob); + if not TempBlob.HasValue() then + Error(MessagePayloadErr, MessageEntryNo); + + EDocMessageContext.Initialize(EDocMessage, TempBlob); + EDocMessageContext.Status().SetStatus("E-Document Service Status"::Sent); + MessageSender := EDocumentService."Service Integration V2"; + MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then + Error(MessageSendingErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + EDocMessage.Status := EDocMessage.Status::Sent; + EDocMessage.Modify(); + end; + local procedure InsertDataStorage(TempBlob: Codeunit "Temp Blob"): Integer var EDocDataStorage: Record "E-Doc. Data Storage"; @@ -96,4 +131,8 @@ codeunit 6433 "E-Doc. Message Mgt." EDocRecRef.Modify(); exit(EDocDataStorage."Entry No."); end; + + var + MessagePayloadErr: Label 'E-Document message %1 does not contain a payload.', Comment = '%1 = E-Document message entry number'; + MessageSendingErr: Label 'E-Document message %1 could not be sent. The integration returned status %2.', Comment = '%1 = E-Document message entry number, %2 = integration status'; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al new file mode 100644 index 00000000000..4b146cc1b46 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration.Interfaces; + +codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context") + begin + Error(MessageTransportNotSupportedErr, EDocumentService.Code); + end; + + var + MessageTransportNotSupportedErr: Label 'E-Document service %1 does not support sending E-Document messages.', Comment = '%1 = E-Document service code'; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al new file mode 100644 index 00000000000..17ca7915849 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument; +using System.Utilities; + +codeunit 6532 "E-Document Message API" +{ + Access = Public; + InherentEntitlements = X; + + procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.CreateMessage(EDocument, MessageType, EDocument.Direction::Outgoing, ResponseType, TempBlob)); + end; + + procedure SendMessage(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.SendMessage(MessageEntryNo); + end; +} From 31b4bb0d0645308d70dccad9c4b0443434563479 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 11:19:26 +0200 Subject: [PATCH 02/49] Add French collected and refused messages --- .../app/src/Core/FREInvoiceMessage.Table.al | 105 ++++++++ .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 251 ++++++++++++++++++ .../src/Core/FREInvoiceMessageType.Enum.al | 23 ++ .../src/Core/FREInvoiceRefusalDialog.Page.al | 43 +++ .../EReportingEDocuments.PageExt.al | 29 ++ .../Extensions/FREDocResponseType.EnumExt.al | 15 ++ .../FREDocumentMessageType.EnumExt.al | 15 ++ .../src/FREDocMessageSenderMock.Codeunit.al | 75 ++++++ .../src/FREInvoiceMessageTests.Codeunit.al | 222 ++++++++++++++++ .../test/src/FRServiceIntegration.EnumExt.al | 16 ++ 10 files changed, 794 insertions(+) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceRefusalDialog.Page.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/test/src/FRServiceIntegration.EnumExt.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al new file mode 100644 index 00000000000..3c0058c7fbf --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -0,0 +1,105 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; + +table 10970 "FR E-Invoice Message" +{ + Caption = 'FR E-Invoice Message'; + DataClassification = CustomerContent; + InherentEntitlements = X; + InherentPermissions = X; + ReplicateData = false; + + fields + { + field(1; "Entry No."; Integer) + { + AutoIncrement = true; + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + } + field(2; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Document"."Entry No"; + } + field(3; Type; Enum "FR E-Invoice Message Type") + { + Caption = 'Type'; + DataClassification = SystemMetadata; + } + field(4; "Source Occurrence ID"; Guid) + { + Caption = 'Source Occurrence ID'; + DataClassification = SystemMetadata; + } + field(5; "Original Entry No."; Integer) + { + Caption = 'Original Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Message"."Entry No."; + } + field(6; Amount; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Amount'; + DataClassification = CustomerContent; + } + field(7; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + } + field(8; "Event Date"; Date) + { + Caption = 'Event Date'; + DataClassification = CustomerContent; + } + field(9; "Detailed Ledger Entry No."; Integer) + { + Caption = 'Detailed Ledger Entry No.'; + DataClassification = SystemMetadata; + } + field(10; "Reason Code"; Code[20]) + { + Caption = 'Reason Code'; + DataClassification = CustomerContent; + } + field(11; "Reason Description"; Text[500]) + { + Caption = 'Reason Description'; + DataClassification = CustomerContent; + } + field(12; "E-Document Message Entry No."; Integer) + { + Caption = 'E-Document Message Entry No.'; + DataClassification = SystemMetadata; + } + field(13; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + key(Occurrence; "E-Document Entry No.", "Source Occurrence ID", Type) + { + Unique = true; + } + key(DetailedLedgerEntry; Type, "Detailed Ledger Entry No.") + { + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al new file mode 100644 index 00000000000..2b1597d41d3 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -0,0 +1,251 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.ReceivablesPayables; +using Microsoft.Sales.Customer; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; +using System.Utilities; + +codeunit 10975 "FR E-Invoice Message Mgt." +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) + begin + ProcessApplication(DtldCustLedgEntry); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); + end; + + internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500]) + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + begin + EDocument.TestField(Direction, EDocument.Direction::Incoming); + EDocument.TestField("Document Type", EDocument."Document Type"::"Purchase Invoice"); + EDocument.TestField(Service); + if ReasonCode = '' then + Error(ReasonCodeRequiredErr); + if ReasonDescription = '' then + Error(ReasonDescriptionRequiredErr); + + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Refused); + if not FREInvoiceMessage.IsEmpty() then + Error(AlreadyRefusedErr, EDocument."Document No."); + + CreateAndSendMessage(EDocument, FREInvoiceMessage.Type::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription); + end; + + internal procedure ProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + EDocument: Record "E-Document"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + begin + if not IsInvoiceApplication(DetailedCustLedgEntry) then + exit; + if not InvoiceCustLedgerEntry.Get(DetailedCustLedgEntry."Cust. Ledger Entry No.") then + exit; + if not PaymentCustLedgerEntry.Get(DetailedCustLedgEntry."Applied Cust. Ledger Entry No.") then + exit; + if PaymentCustLedgerEntry."Document Type" <> PaymentCustLedgerEntry."Document Type"::Payment then + exit; + if not FindInvoiceEDocuments(EDocument, InvoiceCustLedgerEntry) then + exit; + + repeat + if IsEligibleFrenchEDocument(EDocument) then + CreateAndSendMessage( + EDocument, "FR E-Invoice Message Type"::Collected, DetailedCustLedgEntry.SystemId, + -DetailedCustLedgEntry.Amount, DetailedCustLedgEntry."Currency Code", DetailedCustLedgEntry."Posting Date", + DetailedCustLedgEntry."Entry No.", 0, '', ''); + until EDocument.Next() = 0; + end; + + internal procedure ProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + CollectedMessage: Record "FR E-Invoice Message"; + EDocument: Record "E-Document"; + begin + if not IsInvoiceApplication(OldDetailedCustLedgEntry) then + exit; + + CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); + CollectedMessage.SetRange("Detailed Ledger Entry No.", OldDetailedCustLedgEntry."Entry No."); + if not CollectedMessage.FindSet() then + exit; + + repeat + EDocument.Get(CollectedMessage."E-Document Entry No."); + CreateAndSendMessage( + EDocument, "FR E-Invoice Message Type"::"Negative Collected", NewDetailedCustLedgEntry.SystemId, + -CollectedMessage.Amount, CollectedMessage."Currency Code", NewDetailedCustLedgEntry."Posting Date", + NewDetailedCustLedgEntry."Entry No.", CollectedMessage."Entry No.", '', ''); + until CollectedMessage.Next() = 0; + end; + + local procedure CreateAndSendMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalEntryNo: Integer; ReasonCode: Code[20]; ReasonDescription: Text[500]) + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + TempBlob: Codeunit "Temp Blob"; + begin + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange("Source Occurrence ID", SourceOccurrenceID); + FREInvoiceMessage.SetRange(Type, MessageType); + if FREInvoiceMessage.FindFirst() then + exit; + + FREInvoiceMessage.Init(); + FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; + FREInvoiceMessage.Type := MessageType; + FREInvoiceMessage."Source Occurrence ID" := SourceOccurrenceID; + FREInvoiceMessage."Original Entry No." := OriginalEntryNo; + FREInvoiceMessage.Amount := Amount; + FREInvoiceMessage."Currency Code" := CurrencyCode; + FREInvoiceMessage."Event Date" := EventDate; + FREInvoiceMessage."Detailed Ledger Entry No." := DetailedLedgerEntryNo; + FREInvoiceMessage."Reason Code" := ReasonCode; + FREInvoiceMessage."Reason Description" := ReasonDescription; + FREInvoiceMessage."Created At" := CurrentDateTime(); + FREInvoiceMessage.Insert(); + + BuildMessage(EDocument, FREInvoiceMessage, TempBlob); + FREInvoiceMessage."E-Document Message Entry No." := EDocumentMessageAPI.CreateMessage( + EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", GetResponseType(MessageType), TempBlob); + FREInvoiceMessage.Modify(); + EDocumentMessageAPI.SendMessage(FREInvoiceMessage."E-Document Message Entry No."); + end; + + local procedure BuildMessage(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; var TempBlob: Codeunit "Temp Blob") + var + XmlDoc: XmlDocument; + RootElement: XmlElement; + AcknowledgementElement: XmlElement; + ReferenceElement: XmlElement; + StatusElement: XmlElement; + AmountElement: XmlElement; + OutStream: OutStream; + begin + XmlDoc := XmlDocument.Create(); + XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); + RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); + RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok, + XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); + + AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); + ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); + ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); + ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); + if FREInvoiceMessage.Type = FREInvoiceMessage.Type::Refused then begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); + StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); + ReferenceElement.Add(StatusElement); + end else begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); + AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); + StatusElement.Add(AmountElement); + ReferenceElement.Add(StatusElement); + end; + AcknowledgementElement.Add(ReferenceElement); + RootElement.Add(AcknowledgementElement); + XmlDoc.Add(RootElement); + + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + XmlDoc.WriteTo(OutStream); + end; + + local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + if InvoiceCustLedgerEntry."Document Type" <> InvoiceCustLedgerEntry."Document Type"::Invoice then + exit(false); + if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then + exit(false); + + EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); + EDocument.SetRange(Direction, EDocument.Direction::Outgoing); + EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); + exit(EDocument.FindSet()); + end; + + local procedure IsEligibleFrenchEDocument(EDocument: Record "E-Document"): Boolean + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + if not EDocumentService.Get(EDocument.Service) then + exit(false); + if not (EDocumentService."Document Format" in [EDocumentService."Document Format"::"Peppol BIS 3.0 FR", EDocumentService."Document Format"::"Factur-X FR"]) then + exit(false); + if not EDocumentServiceStatus.Get(EDocument."Entry No", EDocument.Service) then + exit(false); + exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]); + end; + + local procedure IsInvoiceApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean + begin + exit( + (DetailedCustLedgEntry."Entry Type" = DetailedCustLedgEntry."Entry Type"::Application) and + (DetailedCustLedgEntry."Initial Document Type" = DetailedCustLedgEntry."Initial Document Type"::Invoice) and + (DetailedCustLedgEntry.Amount < 0)); + end; + + local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type" + begin + if MessageType = MessageType::Refused then + exit("E-Doc. Response Type"::Refused); + exit("E-Doc. Response Type"::None); + end; + + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyCode <> '' then + exit(CurrencyCode); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + exit(GeneralLedgerSetup."LCY Code"); + end; + + var + RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; + RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + InvoiceReferenceStatusCodeTok: Label '47', Locked = true; + CollectedStatusCodeTok: Label '212', Locked = true; + CollectedStatusNameTok: Label 'Encaissée', Locked = true; + CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; + RefusedStatusCodeTok: Label '210', Locked = true; + RefusedStatusNameTok: Label 'Refusée', Locked = true; + ReasonCodeRequiredErr: Label 'A refusal reason code is required.'; + ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.'; + AlreadyRefusedErr: Label 'Invoice %1 has already been refused.', Comment = '%1 = invoice number'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al new file mode 100644 index 00000000000..dae5310dce4 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +enum 10970 "FR E-Invoice Message Type" +{ + Access = Public; + + value(0; Collected) + { + Caption = 'Collected'; + } + value(1; "Negative Collected") + { + Caption = 'Negative Collected'; + } + value(2; Refused) + { + Caption = 'Refused'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceRefusalDialog.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceRefusalDialog.Page.al new file mode 100644 index 00000000000..5d840dd0314 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceRefusalDialog.Page.al @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +page 10972 "FR E-Invoice Refusal Dialog" +{ + Caption = 'Refuse E-Invoice'; + PageType = StandardDialog; + + layout + { + area(Content) + { + field(ReasonCode; ReasonCode) + { + ApplicationArea = Basic, Suite; + Caption = 'Reason Code'; + NotBlank = true; + ToolTip = 'Specifies the code that identifies why the invoice is refused.'; + } + field(ReasonDescription; ReasonDescription) + { + ApplicationArea = Basic, Suite; + Caption = 'Reason Description'; + MultiLine = true; + NotBlank = true; + ToolTip = 'Specifies why the invoice is refused.'; + } + } + } + + internal procedure GetReason(var NewReasonCode: Code[20]; var NewReasonDescription: Text[500]) + begin + NewReasonCode := ReasonCode; + NewReasonDescription := ReasonDescription; + end; + + var + ReasonCode: Code[20]; + ReasonDescription: Text[500]; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al index e2d2204b402..4bdaf743097 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al @@ -20,4 +20,33 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" } } } + + actions + { + addlast(Processing) + { + action(RefuseFREInvoice) + { + ApplicationArea = Basic, Suite; + Caption = 'Refuse E-Invoice'; + Image = Reject; + ToolTip = 'Refuse the incoming French electronic purchase invoice and send the response to the supplier.'; + Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice"); + + trigger OnAction() + var + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + FREInvoiceRefusalDialog: Page "FR E-Invoice Refusal Dialog"; + ReasonCode: Code[20]; + ReasonDescription: Text[500]; + begin + if FREInvoiceRefusalDialog.RunModal() <> Action::OK then + exit; + FREInvoiceRefusalDialog.GetReason(ReasonCode, ReasonDescription); + FREInvoiceMessageMgt.RefuseInvoice(Rec, ReasonCode, ReasonDescription); + CurrPage.Update(false); + end; + } + } + } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al new file mode 100644 index 00000000000..cae8ea1e412 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument.Processing.Message; + +enumextension 10974 "FR E-Doc. Response Type" extends "E-Doc. Response Type" +{ + value(10971; Refused) + { + Caption = 'Refused'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al new file mode 100644 index 00000000000..cceabfb265c --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocumentMessageType.EnumExt.al @@ -0,0 +1,15 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument.Processing.Message; + +enumextension 10973 "FR E-Document Message Type" extends "E-Document Message Type" +{ + value(10970; "FR Invoice Lifecycle") + { + Caption = 'FR Invoice Lifecycle'; + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al new file mode 100644 index 00000000000..1c890b2f63c --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al @@ -0,0 +1,75 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration.Interfaces; +using Microsoft.eServices.EDocument.Integration.Receive; +using Microsoft.eServices.EDocument.Integration.Send; +using Microsoft.eServices.EDocument.Processing.Message; +using System.Utilities; + +codeunit 148150 "FR E-Doc. Msg. Sender Mock" implements IDocumentSender, IDocumentReceiver, IConsentManager, IMessageSender +{ + Access = Internal; + SingleInstance = true; + + procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) + begin + end; + + procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context") + var + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + begin + SendCount += 1; + LastResponseType := MessageContext.GetResponseType(); + TempBlob := MessageContext.GetTempBlob(); + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + InStream.ReadText(LastPayload); + MessageContext.Status().SetStatus("E-Document Service Status"::Sent); + end; + + procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) + begin + end; + + procedure DownloadDocument(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; DocumentMetadata: Codeunit "Temp Blob"; ReceiveContext: Codeunit ReceiveContext) + begin + end; + + procedure ObtainPrivacyConsent(): Boolean + begin + exit(true); + end; + + procedure Reset() + begin + Clear(LastPayload); + Clear(LastResponseType); + SendCount := 0; + end; + + procedure GetSendCount(): Integer + begin + exit(SendCount); + end; + + procedure GetLastPayload(): Text + begin + exit(LastPayload); + end; + + procedure GetLastResponseType(): Enum "E-Doc. Response Type" + begin + exit(LastResponseType); + end; + + var + LastResponseType: Enum "E-Doc. Response Type"; + LastPayload: Text; + SendCount: Integer; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al new file mode 100644 index 00000000000..84f1dbb9411 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -0,0 +1,222 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; + +codeunit 148152 "FR E-Invoice Message Tests" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + Permissions = tabledata "Cust. Ledger Entry" = rimd, + tabledata "Detailed Cust. Ledg. Entry" = rimd, + tabledata "E-Document" = rimd, + tabledata "E-Document Service" = rimd, + tabledata "E-Document Service Status" = rimd, + tabledata "FR E-Invoice Message" = rimd, + tabledata "Sales Invoice Header" = rimd; + + var + Assert: Codeunit Assert; + MessageSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock"; + + [Test] + procedure PaymentApplicationSendsCollected() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 1); + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('212'), 'The payload must contain status 212.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('100'), 'The payload must contain the collected amount.'); + end; + + [Test] + procedure PaymentUnapplicationSendsLinkedNegativeCollected() + var + EDocument: Record "E-Document"; + CollectedMessage: Record "FR E-Invoice Message"; + NegativeMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -100); + + FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + + CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); + CollectedMessage.FindFirst(); + NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + NegativeMessage.SetRange(Type, NegativeMessage.Type::"Negative Collected"); + NegativeMessage.FindFirst(); + Assert.AreEqual(CollectedMessage."Entry No.", NegativeMessage."Original Entry No.", 'The reversal must link to the original occurrence.'); + Assert.AreEqual(-CollectedMessage.Amount, NegativeMessage.Amount, 'The reversal amount must negate the original amount.'); + Assert.AreEqual(2, MessageSenderMock.GetSendCount(), 'Collected and Negative Collected messages must be sent.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('-100'), 'The reversal payload must contain a negative amount.'); + end; + + [Test] + procedure RefusalSendsStatusAndReason() + var + EDocument: Record "E-Document"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + + FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'PRICE', 'The amount is incorrect.'); + + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One refusal message must be sent.'); + Assert.AreEqual("E-Doc. Response Type"::Refused, MessageSenderMock.GetLastResponseType(), 'The child message must be Refused.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('210'), 'The payload must contain status 210.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('PRICE'), 'The payload must contain the reason code.'); + end; + + [Test] + procedure RefusalRequiresReasonAndCannotBeRepeated() + var + EDocument: Record "E-Document"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + + asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', 'Not accepted.'); + Assert.ExpectedError('A refusal reason code is required.'); + FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); + asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Again.'); + Assert.ExpectedError('has already been refused'); + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'A duplicate refusal must not be sent.'); + end; + + local procedure Initialize() + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + begin + FREInvoiceMessage.DeleteAll(); + MessageSenderMock.Reset(); + EnsureService(); + end; + + local procedure EnsureService() + var + EDocumentService: Record "E-Document Service"; + begin + if EDocumentService.Get('FR-MESSAGE-MOCK') then + exit; + EDocumentService.Init(); + EDocumentService.Code := 'FR-MESSAGE-MOCK'; + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"FR Message Mock"; + EDocumentService.Insert(); + end; + + local procedure CreateIncomingEDocument(var EDocument: Record "E-Document") + begin + EDocument.Init(); + EDocument."Document No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(EDocument."Document No.")); + EDocument.Direction := EDocument.Direction::Incoming; + EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + end; + + local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + DocumentNo: Code[20]; + begin + DocumentNo := CopyStr(Format(CreateGuid()), 1, MaxStrLen(DocumentNo)); + SalesInvoiceHeader.Init(); + SalesInvoiceHeader."No." := DocumentNo; + SalesInvoiceHeader.Insert(); + + EDocument.Init(); + EDocument."Document No." := DocumentNo; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + CreateServiceStatus(EDocument); + + InvoiceCustLedgerEntry.Init(); + InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; + InvoiceCustLedgerEntry."Document No." := DocumentNo; + InvoiceCustLedgerEntry.Insert(); + PaymentCustLedgerEntry.Init(); + PaymentCustLedgerEntry."Entry No." := InvoiceCustLedgerEntry."Entry No." + 1; + PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; + PaymentCustLedgerEntry.Insert(); + CreateDetailedLedgerEntry(DetailedCustLedgEntry, InvoiceCustLedgerEntry."Entry No.", PaymentCustLedgerEntry."Entry No.", -100); + end; + + local procedure CreateServiceStatus(EDocument: Record "E-Document") + var + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + EDocumentServiceStatus.Init(); + EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No"; + EDocumentServiceStatus."E-Document Service Code" := EDocument.Service; + EDocumentServiceStatus.Status := EDocumentServiceStatus.Status::Approved; + EDocumentServiceStatus.Insert(); + end; + + local procedure CreateDetailedLedgerEntry(var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; InvoiceEntryNo: Integer; PaymentEntryNo: Integer; Amount: Decimal) + begin + DetailedCustLedgEntry.Init(); + DetailedCustLedgEntry."Entry No." := GetNextDetailedLedgerEntryNo(); + DetailedCustLedgEntry."Cust. Ledger Entry No." := InvoiceEntryNo; + DetailedCustLedgEntry."Applied Cust. Ledger Entry No." := PaymentEntryNo; + DetailedCustLedgEntry."Entry Type" := DetailedCustLedgEntry."Entry Type"::Application; + DetailedCustLedgEntry."Initial Document Type" := DetailedCustLedgEntry."Initial Document Type"::Invoice; + DetailedCustLedgEntry.Amount := Amount; + DetailedCustLedgEntry."Currency Code" := 'EUR'; + DetailedCustLedgEntry."Posting Date" := WorkDate(); + DetailedCustLedgEntry.Insert(); + end; + + local procedure GetNextCustLedgerEntryNo(): Integer + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + if CustLedgerEntry.FindLast() then + exit(CustLedgerEntry."Entry No." + 1); + exit(1); + end; + + local procedure GetNextDetailedLedgerEntryNo(): Integer + var + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + begin + if DetailedCustLedgEntry.FindLast() then + exit(DetailedCustLedgEntry."Entry No." + 1); + exit(1); + end; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FRServiceIntegration.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FRServiceIntegration.EnumExt.al new file mode 100644 index 00000000000..de8a5a7279e --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FRServiceIntegration.EnumExt.al @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats.Test; + +using Microsoft.eServices.EDocument.Integration; +using Microsoft.eServices.EDocument.Integration.Interfaces; + +enumextension 148151 "FR Service Integration" extends "Service Integration" +{ + value(148150; "FR Message Mock") + { + Implementation = IDocumentSender = "FR E-Doc. Msg. Sender Mock", IDocumentReceiver = "FR E-Doc. Msg. Sender Mock", IConsentManager = "FR E-Doc. Msg. Sender Mock", IMessageSender = "FR E-Doc. Msg. Sender Mock"; + } +} \ No newline at end of file From 31387e63f154f3cbbd6930fb82e99ebc8c2cdd5e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 14:22:18 +0200 Subject: [PATCH 03/49] new --- .../Core/FREInvoiceMessageBuilder.Codeunit.al | 84 ++ .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 173 +-- .../src/Core/FREInvoiceMessageType.Enum.al | 12 + .../app/src/Core/FREInvoiceMessages.Page.al | 79 + .../EReportingEDocuments.PageExt.al | 9 + .../src/FREDocMessageSenderMock.Codeunit.al | 10 +- .../src/FREInvoiceMessageTests.Codeunit.al | 145 +- .../test/src/FacturXCIIXMLTests.Codeunit.al | 1289 +++++++++++++++-- .../EDocCoreObjects.PermissionSet.al | 5 + .../Permissions/EDocCoreRead.PermissionSet.al | 2 + .../Permissions/EDocCoreUser.PermissionSet.al | 2 + .../Interfaces/IMessageSender.Interface.al | 10 + .../Integration/ServiceIntegration.Enum.al | 1 + .../EDocumentBackgroundJobs.Codeunit.al | 6 + .../Message/EDocExternalReference.Table.al | 61 + .../Message/EDocMessageContext.Codeunit.al | 24 + .../Message/EDocMessageMgt.Codeunit.al | 91 +- .../Message/EDocMessageSendJob.Codeunit.al | 46 + .../Message/EDocMessageStatus.Enum.al | 12 + .../Message/EDocPaymentOccurrence.Table.al | 94 ++ .../EDocPaymentOccurrenceMgt.Codeunit.al | 154 ++ .../Message/EDocPaymentOccurrenceType.Enum.al | 23 + .../Message/EDocumentMessage.Table.al | 33 + .../Message/EDocumentMessageAPI.Codeunit.al | 54 + .../Message/EDocumentMessagesFactBox.Page.al | 25 + 25 files changed, 2214 insertions(+), 230 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al new file mode 100644 index 00000000000..834166558a0 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -0,0 +1,84 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; +using Microsoft.Finance.GeneralLedger.Setup; +using System.Utilities; + +codeunit 10976 "FR E-Invoice Message Builder" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure BuildMessage(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; var TempBlob: Codeunit "Temp Blob") + var + XmlDoc: XmlDocument; + RootElement: XmlElement; + AcknowledgementElement: XmlElement; + ReferenceElement: XmlElement; + StatusElement: XmlElement; + AmountElement: XmlElement; + OutStream: OutStream; + begin + XmlDoc := XmlDocument.Create(); + XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); + RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); + RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok, + XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); + + AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); + ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); + ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); + ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); + if FREInvoiceMessage.Type = FREInvoiceMessage.Type::Refused then begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); + StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); + ReferenceElement.Add(StatusElement); + end else begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); + AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); + StatusElement.Add(AmountElement); + ReferenceElement.Add(StatusElement); + end; + AcknowledgementElement.Add(ReferenceElement); + RootElement.Add(AcknowledgementElement); + XmlDoc.Add(RootElement); + + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + XmlDoc.WriteTo(OutStream); + end; + + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyCode <> '' then + exit(CurrencyCode); + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + exit(GeneralLedgerSetup."LCY Code"); + end; + + var + RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; + RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + InvoiceReferenceStatusCodeTok: Label '47', Locked = true; + CollectedStatusCodeTok: Label '212', Locked = true; + CollectedStatusNameTok: Label 'Encaissée', Locked = true; + CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; + RefusedStatusCodeTok: Label '210', Locked = true; + RefusedStatusNameTok: Label 'Refusée', Locked = true; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 2b1597d41d3..6f2045d1eaa 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -6,12 +6,6 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Message; -using Microsoft.Finance.GeneralLedger.Journal; -using Microsoft.Finance.GeneralLedger.Posting; -using Microsoft.Finance.GeneralLedger.Setup; -using Microsoft.Finance.ReceivablesPayables; -using Microsoft.Sales.Customer; -using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -21,18 +15,6 @@ codeunit 10975 "FR E-Invoice Message Mgt." InherentEntitlements = X; InherentPermissions = X; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] - local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) - begin - ProcessApplication(DtldCustLedgEntry); - end; - - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] - local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") - begin - ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); - end; - internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500]) var FREInvoiceMessage: Record "FR E-Invoice Message"; @@ -55,56 +37,56 @@ codeunit 10975 "FR E-Invoice Message Mgt." internal procedure ProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") var - EDocument: Record "E-Document"; - InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; - PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin - if not IsInvoiceApplication(DetailedCustLedgEntry) then - exit; - if not InvoiceCustLedgerEntry.Get(DetailedCustLedgEntry."Cust. Ledger Entry No.") then - exit; - if not PaymentCustLedgerEntry.Get(DetailedCustLedgEntry."Applied Cust. Ledger Entry No.") then - exit; - if PaymentCustLedgerEntry."Document Type" <> PaymentCustLedgerEntry."Document Type"::Payment then - exit; - if not FindInvoiceEDocuments(EDocument, InvoiceCustLedgerEntry) then - exit; - - repeat - if IsEligibleFrenchEDocument(EDocument) then - CreateAndSendMessage( - EDocument, "FR E-Invoice Message Type"::Collected, DetailedCustLedgEntry.SystemId, - -DetailedCustLedgEntry.Amount, DetailedCustLedgEntry."Currency Code", DetailedCustLedgEntry."Posting Date", - DetailedCustLedgEntry."Entry No.", 0, '', ''); - until EDocument.Next() = 0; + EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); end; internal procedure ProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + EDocPaymentOccurrenceMgt.ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)] + local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") var CollectedMessage: Record "FR E-Invoice Message"; EDocument: Record "E-Document"; + OriginalOccurrence: Record "E-Doc. Payment Occurrence"; begin - if not IsInvoiceApplication(OldDetailedCustLedgEntry) then + EDocument.Get(EDocPaymentOccurrence."E-Document Entry No."); + if not IsEligibleFrenchEDocument(EDocument) then exit; + if EDocPaymentOccurrence.Type = EDocPaymentOccurrence.Type::Applied then begin + CreateAndSendMessage( + EDocument, "FR E-Invoice Message Type"::Collected, EDocPaymentOccurrence."Source Occurrence ID", + EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date", + EDocPaymentOccurrence."Detailed Ledger Entry No.", 0, '', ''); + exit; + end; + + if not OriginalOccurrence.Get(EDocPaymentOccurrence."Original Occurrence Entry No.") then + exit; + CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); - CollectedMessage.SetRange("Detailed Ledger Entry No.", OldDetailedCustLedgEntry."Entry No."); - if not CollectedMessage.FindSet() then + CollectedMessage.SetRange("Source Occurrence ID", OriginalOccurrence."Source Occurrence ID"); + if not CollectedMessage.FindFirst() then exit; - repeat - EDocument.Get(CollectedMessage."E-Document Entry No."); - CreateAndSendMessage( - EDocument, "FR E-Invoice Message Type"::"Negative Collected", NewDetailedCustLedgEntry.SystemId, - -CollectedMessage.Amount, CollectedMessage."Currency Code", NewDetailedCustLedgEntry."Posting Date", - NewDetailedCustLedgEntry."Entry No.", CollectedMessage."Entry No.", '', ''); - until CollectedMessage.Next() = 0; + CreateAndSendMessage( + EDocument, "FR E-Invoice Message Type"::"Negative Collected", EDocPaymentOccurrence."Source Occurrence ID", + EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date", + EDocPaymentOccurrence."Detailed Ledger Entry No.", CollectedMessage."Entry No.", '', ''); end; local procedure CreateAndSendMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalEntryNo: Integer; ReasonCode: Code[20]; ReasonDescription: Text[500]) var FREInvoiceMessage: Record "FR E-Invoice Message"; EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageBuilder: Codeunit "FR E-Invoice Message Builder"; TempBlob: Codeunit "Temp Blob"; begin FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -127,73 +109,11 @@ codeunit 10975 "FR E-Invoice Message Mgt." FREInvoiceMessage."Created At" := CurrentDateTime(); FREInvoiceMessage.Insert(); - BuildMessage(EDocument, FREInvoiceMessage, TempBlob); + FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); FREInvoiceMessage."E-Document Message Entry No." := EDocumentMessageAPI.CreateMessage( EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", GetResponseType(MessageType), TempBlob); FREInvoiceMessage.Modify(); - EDocumentMessageAPI.SendMessage(FREInvoiceMessage."E-Document Message Entry No."); - end; - - local procedure BuildMessage(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; var TempBlob: Codeunit "Temp Blob") - var - XmlDoc: XmlDocument; - RootElement: XmlElement; - AcknowledgementElement: XmlElement; - ReferenceElement: XmlElement; - StatusElement: XmlElement; - AmountElement: XmlElement; - OutStream: OutStream; - begin - XmlDoc := XmlDocument.Create(); - XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); - RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); - RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); - RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); - RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok, - XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); - - AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); - ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); - ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); - ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); - if FREInvoiceMessage.Type = FREInvoiceMessage.Type::Refused then begin - ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); - ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); - StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); - ReferenceElement.Add(StatusElement); - end else begin - ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); - ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); - AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); - AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); - StatusElement.Add(AmountElement); - ReferenceElement.Add(StatusElement); - end; - AcknowledgementElement.Add(ReferenceElement); - RootElement.Add(AcknowledgementElement); - XmlDoc.Add(RootElement); - - TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); - XmlDoc.WriteTo(OutStream); - end; - - local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean - var - SalesInvoiceHeader: Record "Sales Invoice Header"; - begin - if InvoiceCustLedgerEntry."Document Type" <> InvoiceCustLedgerEntry."Document Type"::Invoice then - exit(false); - if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then - exit(false); - - EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); - EDocument.SetRange(Direction, EDocument.Direction::Outgoing); - EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); - exit(EDocument.FindSet()); + EDocumentMessageAPI.QueueMessage(FREInvoiceMessage."E-Document Message Entry No."); end; local procedure IsEligibleFrenchEDocument(EDocument: Record "E-Document"): Boolean @@ -210,14 +130,6 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]); end; - local procedure IsInvoiceApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean - begin - exit( - (DetailedCustLedgEntry."Entry Type" = DetailedCustLedgEntry."Entry Type"::Application) and - (DetailedCustLedgEntry."Initial Document Type" = DetailedCustLedgEntry."Initial Document Type"::Invoice) and - (DetailedCustLedgEntry.Amount < 0)); - end; - local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type" begin if MessageType = MessageType::Refused then @@ -225,26 +137,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit("E-Doc. Response Type"::None); end; - local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] - var - GeneralLedgerSetup: Record "General Ledger Setup"; - begin - if CurrencyCode <> '' then - exit(CurrencyCode); - GeneralLedgerSetup.Get(); - GeneralLedgerSetup.TestField("LCY Code"); - exit(GeneralLedgerSetup."LCY Code"); - end; - var - RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; - RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; - InvoiceReferenceStatusCodeTok: Label '47', Locked = true; - CollectedStatusCodeTok: Label '212', Locked = true; - CollectedStatusNameTok: Label 'Encaissée', Locked = true; - CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; - RefusedStatusCodeTok: Label '210', Locked = true; - RefusedStatusNameTok: Label 'Refusée', Locked = true; ReasonCodeRequiredErr: Label 'A refusal reason code is required.'; ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.'; AlreadyRefusedErr: Label 'Invoice %1 has already been refused.', Comment = '%1 = invoice number'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al index dae5310dce4..66421e51470 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al @@ -20,4 +20,16 @@ enum 10970 "FR E-Invoice Message Type" { Caption = 'Refused'; } + value(3; Submitted) + { + Caption = 'Submitted'; + } + value(4; "Technical Rejected") + { + Caption = 'Technical Rejected'; + } + value(5; Accepted) + { + Caption = 'Accepted'; + } } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al new file mode 100644 index 00000000000..3816ff3584e --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +page 10973 "FR E-Invoice Messages" +{ + ApplicationArea = Basic, Suite; + Caption = 'French E-Invoice Lifecycle'; + PageType = List; + SourceTable = "FR E-Invoice Message"; + SourceTableView = sorting("Entry No.") order(descending); + Editable = false; + InsertAllowed = false; + DeleteAllowed = false; + ModifyAllowed = false; + UsageCategory = None; + + layout + { + area(content) + { + repeater(Messages) + { + field(Type; Rec.Type) + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the French lifecycle status represented by this message.'; + } + field(Amount; Rec.Amount) + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the payment amount reported by a collected or negative collected message.'; + } + field("Currency Code"; Rec."Currency Code") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the currency of the reported payment amount.'; + } + field("Event Date"; Rec."Event Date") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the business date on which the lifecycle event occurred.'; + } + field("Reason Code"; Rec."Reason Code") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the reason code supplied for the lifecycle status.'; + } + field("Reason Description"; Rec."Reason Description") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the reason description supplied for the lifecycle status.'; + } + field("Source Occurrence ID"; Rec."Source Occurrence ID") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the immutable source identifier used to prevent duplicate lifecycle messages.'; + } + field("Original Entry No."; Rec."Original Entry No.") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the original collected message reversed by a negative collected message.'; + } + field("E-Document Message Entry No."; Rec."E-Document Message Entry No.") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the related generic E-Document message entry.'; + } + field("Created At"; Rec."Created At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the French lifecycle message was created.'; + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al index 4bdaf743097..f89c14b4c33 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al @@ -25,6 +25,15 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" { addlast(Processing) { + action(ViewFREInvoiceLifecycle) + { + ApplicationArea = Basic, Suite; + Caption = 'French E-Invoice Lifecycle'; + Image = History; + ToolTip = 'View the French lifecycle statuses and payment occurrences associated with this E-Document.'; + RunObject = page "FR E-Invoice Messages"; + RunPageLink = "E-Document Entry No." = field("Entry No"); + } action(RefuseFREInvoice) { ApplicationArea = Basic, Suite; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al index 1c890b2f63c..bf0e20961d2 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al @@ -30,7 +30,8 @@ codeunit 148150 "FR E-Doc. Msg. Sender Mock" implements IDocumentSender, IDocume TempBlob := MessageContext.GetTempBlob(); TempBlob.CreateInStream(InStream, TextEncoding::UTF8); InStream.ReadText(LastPayload); - MessageContext.Status().SetStatus("E-Document Service Status"::Sent); + if ReportSuccess then + MessageContext.Status().SetStatus("E-Document Service Status"::Sent); end; procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) @@ -50,9 +51,15 @@ codeunit 148150 "FR E-Doc. Msg. Sender Mock" implements IDocumentSender, IDocume begin Clear(LastPayload); Clear(LastResponseType); + ReportSuccess := true; SendCount := 0; end; + procedure SetReportSuccess(NewReportSuccess: Boolean) + begin + ReportSuccess := NewReportSuccess; + end; + procedure GetSendCount(): Integer begin exit(SendCount); @@ -71,5 +78,6 @@ codeunit 148150 "FR E-Doc. Msg. Sender Mock" implements IDocumentSender, IDocume var LastResponseType: Enum "E-Doc. Response Type"; LastPayload: Text; + ReportSuccess: Boolean; SendCount: Integer; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 84f1dbb9411..6b40e96c8b4 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -10,6 +10,7 @@ using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using System.Utilities; codeunit 148152 "FR E-Invoice Message Tests" { @@ -21,6 +22,7 @@ codeunit 148152 "FR E-Invoice Message Tests" tabledata "E-Document" = rimd, tabledata "E-Document Service" = rimd, tabledata "E-Document Service Status" = rimd, + tabledata "E-Doc. Payment Occurrence" = rimd, tabledata "FR E-Invoice Message" = rimd, tabledata "Sales Invoice Header" = rimd; @@ -32,6 +34,7 @@ codeunit 148152 "FR E-Invoice Message Tests" procedure PaymentApplicationSendsCollected() var EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; @@ -44,6 +47,14 @@ codeunit 148152 "FR E-Invoice Message Tests" FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); Assert.RecordCount(FREInvoiceMessage, 1); + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + EDocPaymentOccurrence.FindFirst(); + Assert.AreEqual(100, EDocPaymentOccurrence.Amount, 'The generic applied occurrence must carry a positive amount.'); + Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Payment posting must queue the message without invoking the connector.'); + FREInvoiceMessage.FindFirst(); + SendMessage(FREInvoiceMessage); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('212'), 'The payload must contain status 212.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('100'), 'The payload must contain the collected amount.'); @@ -54,7 +65,9 @@ codeunit 148152 "FR E-Invoice Message Tests" var EDocument: Record "E-Document"; CollectedMessage: Record "FR E-Invoice Message"; + AppliedOccurrence: Record "E-Doc. Payment Occurrence"; NegativeMessage: Record "FR E-Invoice Message"; + ReversedOccurrence: Record "E-Doc. Payment Occurrence"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; @@ -62,18 +75,29 @@ codeunit 148152 "FR E-Invoice Message Tests" Initialize(); CreatePaymentScenario(EDocument, DetailedCustLedgEntry); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); + CollectedMessage.FindFirst(); + SendMessage(CollectedMessage); CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -100); FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); - CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); - CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); - CollectedMessage.FindFirst(); NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); NegativeMessage.SetRange(Type, NegativeMessage.Type::"Negative Collected"); NegativeMessage.FindFirst(); Assert.AreEqual(CollectedMessage."Entry No.", NegativeMessage."Original Entry No.", 'The reversal must link to the original occurrence.'); Assert.AreEqual(-CollectedMessage.Amount, NegativeMessage.Amount, 'The reversal amount must negate the original amount.'); + AppliedOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + AppliedOccurrence.SetRange(Type, AppliedOccurrence.Type::Applied); + AppliedOccurrence.FindFirst(); + ReversedOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + ReversedOccurrence.SetRange(Type, ReversedOccurrence.Type::Reversed); + ReversedOccurrence.FindFirst(); + Assert.AreEqual(AppliedOccurrence."Entry No.", ReversedOccurrence."Original Occurrence Entry No.", 'The generic reversal must link to its applied occurrence.'); + Assert.AreEqual(-AppliedOccurrence.Amount, ReversedOccurrence.Amount, 'The generic reversal must negate the applied amount.'); + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'Unapplication must queue the reversal without invoking the connector.'); + SendMessage(NegativeMessage); Assert.AreEqual(2, MessageSenderMock.GetSendCount(), 'Collected and Negative Collected messages must be sent.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('-100'), 'The reversal payload must contain a negative amount.'); end; @@ -89,6 +113,8 @@ codeunit 148152 "FR E-Invoice Message Tests" FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'PRICE', 'The amount is incorrect.'); + Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Refusal must queue the message without invoking the connector.'); + SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One refusal message must be sent.'); Assert.AreEqual("E-Doc. Response Type"::Refused, MessageSenderMock.GetLastResponseType(), 'The child message must be Refused.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('210'), 'The payload must contain status 210.'); @@ -107,15 +133,128 @@ codeunit 148152 "FR E-Invoice Message Tests" asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', 'Not accepted.'); Assert.ExpectedError('A refusal reason code is required.'); FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); + SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Again.'); Assert.ExpectedError('has already been refused'); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'A duplicate refusal must not be sent.'); end; + [Test] + procedure MessageSenderMustReportSuccess() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocumentMessageAPI.CreateMessage( + EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, TempBlob); + MessageSenderMock.SetReportSuccess(false); + + asserterror EDocumentMessageAPI.SendMessage(MessageEntryNo); + + Assert.ExpectedError('could not be sent'); + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'The connector must be invoked before its missing success result is rejected.'); + end; + + [Test] + procedure PaymentApplicationReplayIsIdempotent() + var + EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 1); + end; + + [Test] + procedure IncomingMessageIsCorrelatedAndDeduplicated() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + FirstMessageEntryNo: Integer; + DuplicateMessageEntryNo: Integer; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, 'FR-DOC-001'); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + + FirstMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( + EDocument.Service, 'FR-DOC-001', 'FR-MSG-001', "E-Document Message Type"::"FR Invoice Lifecycle", + "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); + DuplicateMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( + EDocument.Service, 'FR-DOC-001', 'FR-MSG-001', "E-Document Message Type"::"FR Invoice Lifecycle", + "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); + + Assert.AreNotEqual(0, FirstMessageEntryNo, 'The incoming lifecycle message must be persisted.'); + Assert.AreEqual(FirstMessageEntryNo, DuplicateMessageEntryNo, 'The external message ID must deduplicate repeated delivery.'); + end; + + [Test] + procedure IncomingMessageRequiresRegisteredDocumentReference() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + + asserterror EDocumentMessageAPI.CreateIncomingMessage( + EDocument.Service, CopyStr(Format(CreateGuid()), 1, 250), CopyStr(Format(CreateGuid()), 1, 250), + "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); + + Assert.ExpectedError('is not registered'); + end; + + local procedure SendFirstMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type") + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + begin + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, MessageType); + FREInvoiceMessage.FindFirst(); + SendMessage(FREInvoiceMessage); + end; + + local procedure SendMessage(FREInvoiceMessage: Record "FR E-Invoice Message") + var + EDocumentMessageAPI: Codeunit "E-Document Message API"; + begin + EDocumentMessageAPI.SendMessage(FREInvoiceMessage."E-Document Message Entry No."); + end; + local procedure Initialize() var + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; begin + EDocPaymentOccurrence.DeleteAll(); FREInvoiceMessage.DeleteAll(); MessageSenderMock.Reset(); EnsureService(); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index d73ef22afad..7b4d69f4a5b 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -6,15 +6,20 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; +using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; using Microsoft.Foundation.UOM; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Location; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.FinanceCharge; using Microsoft.Sales.History; +using Microsoft.Sales.Pricing; using Microsoft.Sales.Reminder; using Microsoft.Sales.Setup; using System.Utilities; @@ -23,6 +28,7 @@ codeunit 148148 "Factur-X CII XML Tests" { Subtype = Test; Permissions = tabledata "Company Information" = rimd, + tabledata "Sales Invoice Header" = m, tabledata Customer = rimd; trigger OnRun() @@ -33,14 +39,18 @@ codeunit 148148 "Factur-X CII XML Tests" var CompanyInformation: Record "Company Information"; LibraryTestInitialize: Codeunit "Library - Test Initialize"; + LibrarySetupStorage: Codeunit "Library - Setup Storage"; LibrarySales: Codeunit "Library - Sales"; + LibraryInventory: Codeunit "Library - Inventory"; LibraryERM: Codeunit "Library - ERM"; LibraryUtility: Codeunit "Library - Utility"; Assert: Codeunit Assert; CIIXMLBuilder: Codeunit "CII XML Builder"; + EDocHelpers: Codeunit "EDoc. Helpers"; + FacturXFormat: Codeunit "Factur-X Format"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; - CustomerVATNoSequence: Integer; + DialogErrorCodeTok: Label 'Dialog', Locked = true; IsInitialized: Boolean; #region SalesInvoice @@ -81,6 +91,25 @@ codeunit 148148 "Factur-X CII XML Tests" StrSubstNo(IncorrectValueErr, '//ram:GuidelineSpecifiedDocumentContextParameter/ram:ID')); end; + [Test] + procedure FacturXSalesInvoiceXMLHasFrenchBillingMode() + var + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML declares the French service billing mode + Initialize(); + + // [GIVEN] Posted sales invoice with a G/L account line + // [WHEN] Create CII XML via FR CII XML Builder + CreateSalesInvoiceCIIXML(TempBlob); + + // [THEN] BusinessProcessSpecifiedDocumentContextParameter/ID = 'S1' + Assert.AreEqual('S1', + GetCIINodeValue(TempBlob, '//ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID'), + StrSubstNo(IncorrectValueErr, '//ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID')); + end; + [Test] procedure FacturXSalesInvoiceXMLHasDocumentNumber() var @@ -248,11 +277,11 @@ codeunit 148148 "Factur-X CII XML Tests" ElecAddress: Text[250]; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML has buyer FR Electronic Address as BuyerTradeParty/URIUniversalCommunication/URIID + // [SCENARIO] Factur-X CII XML has buyer FR Electronic Address as BuyerTradeParty/URIUniversalCommunication/URIID with scheme 0225 Initialize(); - // [GIVEN] Customer with FR Electronic Address - ElecAddress := '98765432101234'; + // [GIVEN] Customer with FR Electronic Address in SIREN_suffix format + ElecAddress := '987654321_001'; SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithElecAddress(ElecAddress)); Customer.Get(SalesInvoiceHeader."Sell-to Customer No."); @@ -263,33 +292,38 @@ codeunit 148148 "Factur-X CII XML Tests" Assert.AreEqual(ElecAddress, GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID'), StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID')); + + // [THEN] schemeID = '0225' + Assert.AreEqual('0225', + GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); end; [Test] - procedure FacturXSalesInvoiceXMLBuyerFallsBackToVATNoWhenFRElecAddressBlank() + procedure FacturXSalesInvoiceXMLBuyerFallsBackToRegistrationNumber() var SalesInvoiceHeader: Record "Sales Invoice Header"; Customer: Record Customer; TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML uses buyer VAT Registration No. with schemeID 9957 as fallback when FR Electronic Address is blank (BR-FR-12) + // [SCENARIO] Factur-X CII XML uses buyer Registration Number (first 9 digits) with schemeID 0225 when FR Electronic Address is blank Initialize(); - // [GIVEN] Posted sales invoice with customer having no FR Electronic Address but having a VAT Registration No. + // [GIVEN] Posted sales invoice with customer having no FR Electronic Address but having a Registration Number SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); Customer.Get(SalesInvoiceHeader."Sell-to Customer No."); // [WHEN] Create CII XML CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); - // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID = customer VAT Registration No. (BR-FR-12 fallback) - Assert.AreEqual(Customer."VAT Registration No.", + // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID = first 9 digits of Registration Number + Assert.AreEqual(CopyStr(Customer."Registration Number", 1, 9), GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID'), StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID')); - // [THEN] schemeID = '9957' - Assert.AreEqual('9957', + // [THEN] schemeID = '0225' + Assert.AreEqual('0225', GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); end; @@ -300,29 +334,50 @@ codeunit 148148 "Factur-X CII XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; GeneralLedgerSetup: Record "General Ledger Setup"; TempBlob: Codeunit "Temp Blob"; - ExpectedCurrencyCode: Code[10]; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML settlement currency code matches the document currency or LCY + // [SCENARIO] Factur-X CII XML settlement currency code uses LCY when document has no currency Initialize(); - // [GIVEN] Posted sales invoice + // [GIVEN] Posted sales invoice with blank Currency Code (uses LCY) SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); GeneralLedgerSetup.Get(); - if SalesInvoiceHeader."Currency Code" <> '' then - ExpectedCurrencyCode := SalesInvoiceHeader."Currency Code" - else - ExpectedCurrencyCode := GeneralLedgerSetup."LCY Code"; // [WHEN] Create CII XML CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); - // [THEN] ApplicableHeaderTradeSettlement/InvoiceCurrencyCode = expected currency - Assert.AreEqual(ExpectedCurrencyCode, + // [THEN] ApplicableHeaderTradeSettlement/InvoiceCurrencyCode = LCY Code + Assert.AreEqual(GeneralLedgerSetup."LCY Code", GetCIINodeValue(TempBlob, '//ram:InvoiceCurrencyCode'), StrSubstNo(IncorrectValueErr, '//ram:InvoiceCurrencyCode')); end; + [Test] + procedure FacturXSalesInvoiceXMLHasDocumentCurrencyCode() + var + Currency: Record Currency; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML settlement currency code uses the document currency + Initialize(); + + // [GIVEN] Sales invoice with a foreign Currency Code + LibraryERM.CreateCurrency(Currency); + LibraryERM.CreateRandomExchangeRate(Currency.Code); + SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesDocumentWithLine("Sales Document Type"::Invoice, '', Currency.Code)); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] InvoiceCurrencyCode equals the document currency + Assert.AreEqual(Currency.Code, GetCIINodeValue(TempBlob, '//ram:InvoiceCurrencyCode'), + StrSubstNo(IncorrectValueErr, '//ram:InvoiceCurrencyCode')); + end; + [Test] procedure FacturXSalesInvoiceXMLHasIssueDateTimeFormat102() var @@ -363,12 +418,10 @@ codeunit 148148 "Factur-X CII XML Tests" // [GIVEN] Company Information with address CompanyInformation.Get(); - if CompanyInformation.Address = '' then begin - CompanyInformation.Address := '123 Test Street'; - CompanyInformation.City := 'Paris'; - CompanyInformation."Post Code" := '75001'; - CompanyInformation.Modify(true); - end; + CompanyInformation.Address := '123 Test Street'; + CompanyInformation.City := 'Paris'; + CompanyInformation."Post Code" := '75001'; + CompanyInformation.Modify(true); // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -465,7 +518,82 @@ codeunit 148148 "Factur-X CII XML Tests" end; [Test] - procedure FacturXSalesInvoiceXMLHasBuyerElecAddressSchemeID() + procedure FacturXSalesInvoiceXMLPreservesLowercaseBuyerVATCountryPrefix() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML preserves a buyer VAT registration starting with a lowercase country prefix + Initialize(); + + // [GIVEN] Posted sales invoice with buyer country "FR" and VAT registration "fr12345678901" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceHeader."Sell-to Country/Region Code" := 'FR'; + SalesInvoiceHeader."VAT Registration No." := 'fr12345678901'; + SalesInvoiceHeader.Modify(); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Buyer VAT registration remains "fr12345678901" + Assert.AreEqual('fr12345678901', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID')); + end; + + [Test] + procedure FacturXSalesInvoiceXMLPrefixesBuyerVATStartingWithLetterAndDigit() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML prefixes a buyer VAT registration whose first two characters are not letters + Initialize(); + + // [GIVEN] Posted sales invoice with buyer country "FR" and VAT registration "F12345678901" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceHeader."Sell-to Country/Region Code" := 'FR'; + SalesInvoiceHeader."VAT Registration No." := 'F12345678901'; + SalesInvoiceHeader.Modify(); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Buyer VAT registration is "FRF12345678901" + Assert.AreEqual('FRF12345678901', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID')); + end; + + [Test] + procedure FacturXSalesInvoiceXMLPrefixesSingleCharacterBuyerVAT() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML prefixes a single-character buyer VAT registration + Initialize(); + + // [GIVEN] Posted sales invoice with buyer country "FR" and VAT registration "1" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceHeader."Sell-to Country/Region Code" := 'FR'; + SalesInvoiceHeader."VAT Registration No." := '1'; + SalesInvoiceHeader.Modify(); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Buyer VAT registration is "FR1" + Assert.AreEqual('FR1', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID')); + end; + + [Test] + procedure FacturXSalesInvoiceXMLHasBuyerElecAddressSchemeID0225() var SalesInvoiceHeader: Record "Sales Invoice Header"; Customer: Record Customer; @@ -473,21 +601,19 @@ codeunit 148148 "Factur-X CII XML Tests" ElecAddress: Text[250]; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML buyer electronic address URIID has schemeID from customer setting + // [SCENARIO] Factur-X CII XML buyer electronic address always has schemeID 0225 regardless of customer configured scheme Initialize(); - // [GIVEN] Customer with FR Electronic Address and a SIRET electronic address scheme - ElecAddress := '98765432101234'; + // [GIVEN] Customer with FR Electronic Address (valid SIREN) + ElecAddress := '987654321'; SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithElecAddress(ElecAddress)); Customer.Get(SalesInvoiceHeader."Sell-to Customer No."); - Customer.Validate("FR Elec. Address Scheme", Customer."FR Elec. Address Scheme"::"0009"); - Customer.Modify(true); // [WHEN] Create CII XML CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); - // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID/@schemeID = bare scheme code (not the enum caption) - Assert.AreEqual('0009', + // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID/@schemeID = '0225' + Assert.AreEqual('0225', GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); end; @@ -524,22 +650,19 @@ codeunit 148148 "Factur-X CII XML Tests" ExpectedDate: Text; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML has ActualDeliverySupplyChainEvent with delivery date (BT-72) + // [SCENARIO] Factur-X CII XML has ActualDeliverySupplyChainEvent with posting date as fallback (BT-72) Initialize(); - // [GIVEN] Posted sales invoice + // [GIVEN] Posted sales invoice with blank Shipment Date SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceHeader."Shipment Date" := 0D; + SalesInvoiceHeader.Modify(); + ExpectedDate := Format(SalesInvoiceHeader."Posting Date", 0, ''); // [WHEN] Create CII XML CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); - // [THEN] ActualDeliverySupplyChainEvent/OccurrenceDateTime/DateTimeString has date - // Delivery date falls back to document date if shipment date is empty - if SalesInvoiceHeader."Shipment Date" <> 0D then - ExpectedDate := Format(SalesInvoiceHeader."Shipment Date", 0, '') - else - ExpectedDate := Format(SalesInvoiceHeader."Posting Date", 0, ''); - + // [THEN] ActualDeliverySupplyChainEvent/OccurrenceDateTime/DateTimeString = posting date as YYYYMMDD Assert.AreEqual(ExpectedDate, GetCIINodeValue(TempBlob, '//ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString'), StrSubstNo(IncorrectValueErr, '//ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString')); @@ -550,6 +673,32 @@ codeunit 148148 "Factur-X CII XML Tests" StrSubstNo(IncorrectValueErr, '//ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString/@format')); end; + [Test] + procedure FacturXSalesInvoiceXMLUsesShipmentDateAsDeliveryDate() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + ShipmentDate: Date; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML uses Shipment Date as the actual delivery date + Initialize(); + + // [GIVEN] Posted sales invoice with a known Shipment Date + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + ShipmentDate := CalcDate('<-1D>', SalesInvoiceHeader."Posting Date"); + SalesInvoiceHeader."Shipment Date" := ShipmentDate; + SalesInvoiceHeader.Modify(); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Actual delivery date equals Shipment Date + Assert.AreEqual(Format(ShipmentDate, 0, ''), + GetCIINodeValue(TempBlob, '//ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString'), + StrSubstNo(IncorrectValueErr, '//ram:ActualDeliverySupplyChainEvent/ram:OccurrenceDateTime/udt:DateTimeString')); + end; + [Test] procedure FacturXSalesInvoiceXMLHasPaymentMeansTypeCode58() var @@ -571,36 +720,118 @@ codeunit 148148 "Factur-X CII XML Tests" [Test] procedure FacturXSalesInvoiceXMLHasTaxBreakdown() var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML has ApplicableTradeTax with VAT type code and category + // [SCENARIO] Factur-X CII XML has ApplicableTradeTax with VAT type code, basis, and category Initialize(); + // [GIVEN] Posted sales invoice with known amounts + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + SalesInvoiceLine.FindFirst(); + // [WHEN] Create CII XML - CreateSalesInvoiceCIIXML(TempBlob); + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); // [THEN] ApplicableTradeTax/TypeCode = 'VAT' Assert.AreEqual('VAT', GetCIINodeValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode'), StrSubstNo(IncorrectValueErr, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:TypeCode')); - // [THEN] ApplicableTradeTax has BasisAmount - Assert.AreNotEqual('', + // [THEN] ApplicableTradeTax/BasisAmount = line amount + Assert.AreEqual(Format(SalesInvoiceLine.Amount, 0, ''), GetCIINodeValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:BasisAmount'), StrSubstNo(IncorrectValueErr, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:BasisAmount')); - // [THEN] ApplicableTradeTax has CategoryCode - Assert.AreNotEqual('', + // [THEN] ApplicableTradeTax has CategoryCode 'S' (standard rate) + Assert.AreEqual('S', GetCIINodeValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode'), StrSubstNo(IncorrectValueErr, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode')); - // [THEN] ApplicableTradeTax has RateApplicablePercent - Assert.AreNotEqual('', + // [THEN] ApplicableTradeTax/RateApplicablePercent = line VAT % + Assert.AreEqual(Format(SalesInvoiceLine."VAT %", 0, ''), GetCIINodeValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent'), StrSubstNo(IncorrectValueErr, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:RateApplicablePercent')); end; + [Test] + procedure FacturXExemptVATBreakdownHasExemptionReasonWithoutVATEXCode() + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + VATPostingSetup: Record "VAT Posting Setup"; + TempBlob: Codeunit "Temp Blob"; + ExemptionReasonXPath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO] An exempt VAT breakdown without a configured VATEX code satisfies BR-E-10 + Initialize(); + + // [GIVEN] VAT Posting Setup with 0% VAT, category 'E', and no VAT Clause + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 0); + VATPostingSetup."Tax Category" := 'E'; + VATPostingSetup."VAT Clause Code" := ''; + VATPostingSetup.Modify(true); + GLAccount.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + GLAccount.Modify(true); + + // [GIVEN] Posted sales invoice with one exempt line + Customer.Get(CreateCustomer('123456789')); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] The category 'E' header VAT breakdown contains fallback exemption reason text + ExemptionReasonXPath := '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode="E"]/ram:ExemptionReason'; + Assert.AreEqual('Exempt from VAT', GetCIINodeValue(TempBlob, ExemptionReasonXPath), + StrSubstNo(IncorrectValueErr, ExemptionReasonXPath)); + Assert.AreEqual(1, GetCIINodeCount(TempBlob, ExemptionReasonXPath + '/following-sibling::ram:BasisAmount'), + StrSubstNo(IncorrectValueErr, 'ExemptionReason must precede BasisAmount')); + end; + + [Test] + procedure FacturXSalesInvoiceCommentLineDoesNotCreateEmptyVATCategory() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + EmptyVATCategoryXPath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO] A comment line does not create a VAT breakdown with an empty category + Initialize(); + + // [GIVEN] Posted sales invoice "SI" with a financial line and a comment line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithComment()); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] The header VAT breakdown does not contain an empty category code + EmptyVATCategoryXPath := '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode[not(normalize-space())]'; + Assert.AreEqual(0, GetCIINodeCount(TempBlob, EmptyVATCategoryXPath), StrSubstNo(IncorrectValueErr, EmptyVATCategoryXPath)); + end; + [Test] procedure FacturXSalesInvoiceXMLHasMonetarySummation() var @@ -650,42 +881,50 @@ codeunit 148148 "Factur-X CII XML Tests" [Test] procedure FacturXSalesInvoiceXMLHasLineItem() var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML has IncludedSupplyChainTradeLineItem with line details + // [SCENARIO] Factur-X CII XML has IncludedSupplyChainTradeLineItem with fixture-derived values Initialize(); + // [GIVEN] Posted sales invoice with known line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + SalesInvoiceLine.FindFirst(); + // [WHEN] Create CII XML - CreateSalesInvoiceCIIXML(TempBlob); + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); - // [THEN] Line has LineID - Assert.AreNotEqual('', + // [THEN] Line has LineID = '1' + Assert.AreEqual('1', GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:AssociatedDocumentLineDocument/ram:LineID'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:AssociatedDocumentLineDocument/ram:LineID')); - // [THEN] Line has product name - Assert.AreNotEqual('', + // [THEN] Line has product name = line description + Assert.AreEqual(SalesInvoiceLine.Description, GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedTradeProduct/ram:Name'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedTradeProduct/ram:Name')); - // [THEN] Line has net price - Assert.AreNotEqual('', - GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount'), + // [THEN] Line has net price = unit price + Assert.AreEqual(SalesInvoiceLine."Unit Price", + GetCIINodeDecimalValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeAgreement/ram:NetPriceProductTradePrice/ram:ChargeAmount')); // [THEN] Line has billed quantity - Assert.AreNotEqual('', + Assert.AreEqual(Format(SalesInvoiceLine.Quantity, 0, ''), GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeDelivery/ram:BilledQuantity')); - // [THEN] Line has tax category code - Assert.AreNotEqual('', + // [THEN] Line has tax category code 'S' + Assert.AreEqual('S', GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode')); - // [THEN] Line has line total amount - Assert.AreNotEqual('', + // [THEN] Line has line total amount = line amount + Assert.AreEqual(Format(SalesInvoiceLine.Amount, 0, ''), GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:SpecifiedTradeSettlementLineMonetarySummation/ram:LineTotalAmount')); end; @@ -771,6 +1010,33 @@ codeunit 148148 "Factur-X CII XML Tests" StrSubstNo(IncorrectValueErr, '//rsm:ExchangedDocument/ram:ID')); end; + [Test] + procedure FacturXSalesCreditMemoXMLHasReferencedInvoiceNumberAndDate() + var + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML for an applied credit memo identifies the referenced invoice and its date + Initialize(); + + // [GIVEN] Posted sales credit memo applied to posted invoice "SI" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoice()); + SalesCrMemoHeader.Get(CreateAndPostSalesCreditMemo(SalesInvoiceHeader)); + + // [WHEN] Create credit memo CII XML + CreateSalesCreditMemoCIIXML(SalesCrMemoHeader, TempBlob); + + // [THEN] InvoiceReferencedDocument contains the invoice number and document date + Assert.AreEqual(SalesInvoiceHeader."No.", + GetCIINodeValue(TempBlob, '//ram:InvoiceReferencedDocument/ram:IssuerAssignedID'), + StrSubstNo(IncorrectValueErr, '//ram:InvoiceReferencedDocument/ram:IssuerAssignedID')); + Assert.AreEqual(Format(SalesInvoiceHeader."Document Date", 0, ''), + GetCIINodeValue(TempBlob, '//ram:InvoiceReferencedDocument/ram:FormattedIssueDateTime/qdt:DateTimeString'), + StrSubstNo(IncorrectValueErr, '//ram:InvoiceReferencedDocument/ram:FormattedIssueDateTime/qdt:DateTimeString')); + end; + [Test] procedure FacturXSalesCreditMemoXMLHasSellerSIRET() var @@ -845,29 +1111,50 @@ codeunit 148148 "Factur-X CII XML Tests" SalesCrMemoHeader: Record "Sales Cr.Memo Header"; GeneralLedgerSetup: Record "General Ledger Setup"; TempBlob: Codeunit "Temp Blob"; - ExpectedCurrencyCode: Code[10]; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X CII XML for a credit memo has settlement currency code + // [SCENARIO] Factur-X CII XML for a credit memo uses LCY when document has no currency Initialize(); - // [GIVEN] Posted sales credit memo + // [GIVEN] Posted sales credit memo with blank Currency Code (uses LCY) SalesCrMemoHeader.Get(CreateAndPostSalesCreditMemo()); GeneralLedgerSetup.Get(); - if SalesCrMemoHeader."Currency Code" <> '' then - ExpectedCurrencyCode := SalesCrMemoHeader."Currency Code" - else - ExpectedCurrencyCode := GeneralLedgerSetup."LCY Code"; // [WHEN] Create credit memo CII XML CreateSalesCreditMemoCIIXML(SalesCrMemoHeader, TempBlob); - // [THEN] InvoiceCurrencyCode = expected currency - Assert.AreEqual(ExpectedCurrencyCode, + // [THEN] InvoiceCurrencyCode = LCY Code + Assert.AreEqual(GeneralLedgerSetup."LCY Code", GetCIINodeValue(TempBlob, '//ram:InvoiceCurrencyCode'), StrSubstNo(IncorrectValueErr, '//ram:InvoiceCurrencyCode')); end; + [Test] + procedure FacturXSalesCreditMemoXMLHasDocumentCurrencyCode() + var + Currency: Record Currency; + SalesHeader: Record "Sales Header"; + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML for a credit memo uses the document currency + Initialize(); + + // [GIVEN] Sales credit memo with a foreign Currency Code + LibraryERM.CreateCurrency(Currency); + LibraryERM.CreateRandomExchangeRate(Currency.Code); + SalesHeader.Get("Sales Document Type"::"Credit Memo", CreateSalesDocumentWithLine("Sales Document Type"::"Credit Memo", '', Currency.Code)); + SalesCrMemoHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Create credit memo CII XML + CreateSalesCreditMemoCIIXML(SalesCrMemoHeader, TempBlob); + + // [THEN] InvoiceCurrencyCode equals the document currency + Assert.AreEqual(Currency.Code, GetCIINodeValue(TempBlob, '//ram:InvoiceCurrencyCode'), + StrSubstNo(IncorrectValueErr, '//ram:InvoiceCurrencyCode')); + end; + [Test] procedure FacturXSalesCreditMemoXMLHasMonetarySummation() var @@ -922,6 +1209,96 @@ codeunit 148148 "Factur-X CII XML Tests" GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedTradeProduct/ram:Name'), StrSubstNo(IncorrectValueErr, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedTradeProduct/ram:Name')); end; + + [Test] + procedure FacturXSalesCrMemoZeroVATCatSPreservedWithGermanBuyer() + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + VATPostingSetup: Record "VAT Posting Setup"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesCrMemoHeader: Record "Sales Cr.Memo Header"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + TempBlob: Codeunit "Temp Blob"; + CustomerNo: Code[20]; + LineTaxCategoryXPath: Text; + HeaderTaxCategoryXPath: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO] Credit memo line with VAT%=0 preserves source Tax Category 'S' and serializes the rate; German buyer gets DE-prefixed VAT ID + Initialize(); + + // [GIVEN] German customer "C" with VAT Registration No. '533435789', FR Electronic Address '123456789_FOREIGN' + EnsureCountryRegionExists('DE'); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("Country/Region Code", 'DE'); + Customer."VAT Registration No." := '533435789'; + Customer."Registration Number" := ''; + Customer."FR Electronic Address" := '123456789_FOREIGN'; + Customer.Modify(true); + CustomerNo := Customer."No."; + + // [GIVEN] VAT Posting Setup with Normal VAT, 0%, Tax Category 'S' + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Credit Memo Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Credit Memo Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 0); + VATPostingSetup."Tax Category" := 'S'; + VATPostingSetup.Modify(true); + GLAccount.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + GLAccount.Modify(true); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + // [GIVEN] Posted sales credit memo "CM" with a single financial line + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + SalesCrMemoHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Create credit memo CII XML + CreateSalesCreditMemoCIIXML(SalesCrMemoHeader, TempBlob); + + // [THEN] Line-level ApplicableTradeTax preserves CategoryCode 'S' and RateApplicablePercent '0' + LineTaxCategoryXPath := '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode'; + Assert.AreEqual('S', GetCIINodeValue(TempBlob, LineTaxCategoryXPath), + StrSubstNo(IncorrectValueErr, LineTaxCategoryXPath)); + Assert.AreEqual('0', + GetCIINodeValue(TempBlob, '//ram:IncludedSupplyChainTradeLineItem/ram:SpecifiedLineTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode="S"]/ram:RateApplicablePercent'), + StrSubstNo(IncorrectValueErr, 'Line RateApplicablePercent')); + + // [THEN] Header ApplicableTradeTax preserves CategoryCode 'S' and RateApplicablePercent '0' + HeaderTaxCategoryXPath := '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CategoryCode'; + Assert.AreEqual('S', GetCIINodeValue(TempBlob, HeaderTaxCategoryXPath), + StrSubstNo(IncorrectValueErr, HeaderTaxCategoryXPath)); + Assert.AreEqual('0', + GetCIINodeValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:CategoryCode="S"]/ram:RateApplicablePercent'), + StrSubstNo(IncorrectValueErr, 'Header RateApplicablePercent')); + Assert.AreEqual(0, GetCIINodeCount(TempBlob, '//ram:ApplicableTradeTax[ram:CategoryCode="O"]'), + StrSubstNo(IncorrectValueErr, 'CategoryCode O must not be derived from zero-rate category S')); + + // [THEN] Buyer SpecifiedTaxRegistration/ID = 'DE533435789' with schemeID 'VA' + Assert.AreEqual('DE533435789', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID')); + Assert.AreEqual('VA', + GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID/@schemeID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:SpecifiedTaxRegistration/ram:ID/@schemeID')); + + // [THEN] Buyer URIID = '123456789_FOREIGN' with schemeID '0225' + Assert.AreEqual('123456789_FOREIGN', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID')); + Assert.AreEqual('0225', + GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); + end; #endregion #region Reminder @@ -1006,6 +1383,414 @@ codeunit 148148 "Factur-X CII XML Tests" end; #endregion + #region BillingMode + [Test] + procedure FacturXBillingModeB1ForItemOnlyInvoice() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; + SourceDocumentLines: RecordRef; + OriginalView: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO] GetFrenchBillingMode returns B1 for an invoice with only Item lines + Initialize(); + + // [GIVEN] Posted sales invoice containing only an Item line + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithBillingModeLines(false)); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SourceDocumentLines.GetTable(SalesInvoiceLine); + OriginalView := SourceDocumentLines.GetView(false); + + // [WHEN] GetFrenchBillingMode is called + // [THEN] Result = 'B1' and the source lines view is unchanged + Assert.AreEqual('B1', PeppolBIS30FRFormat.GetFrenchBillingMode(SourceDocumentLines), + StrSubstNo(IncorrectValueErr, 'BillingMode B1')); + Assert.AreEqual(OriginalView, SourceDocumentLines.GetView(false), StrSubstNo(IncorrectValueErr, 'Source Document Lines View')); + end; + + [Test] + procedure FacturXBillingModeM1ForMixedItemAndNonItemInvoice() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; + SourceDocumentLines: RecordRef; + OriginalView: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO] GetFrenchBillingMode returns M1 for an invoice with both Item and G/L Account lines + Initialize(); + + // [GIVEN] Posted sales invoice containing Item and G/L Account lines + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithBillingModeLines(true)); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SourceDocumentLines.GetTable(SalesInvoiceLine); + OriginalView := SourceDocumentLines.GetView(false); + + // [WHEN] GetFrenchBillingMode is called + // [THEN] Result = 'M1' and the source lines view is unchanged + Assert.AreEqual('M1', PeppolBIS30FRFormat.GetFrenchBillingMode(SourceDocumentLines), + StrSubstNo(IncorrectValueErr, 'BillingMode M1')); + Assert.AreEqual(OriginalView, SourceDocumentLines.GetView(false), StrSubstNo(IncorrectValueErr, 'Source Document Lines View')); + end; + #endregion + + #region Validation + [Test] + procedure FacturXCheckRaisesErrorWhenBuyerElectronicAddressIsMissing() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SourceDocumentHeader: RecordRef; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X Format Check raises error when buyer has no electronic address or VAT + Initialize(); + + // [GIVEN] Posted sales invoice for customer "C" without electronic address or VAT + CustomerNo := CreateCustomerWithoutIdentifiers(); + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceForCustomer(CustomerNo)); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + + // [WHEN] Factur-X Format Check is called + asserterror CheckFacturX(SourceDocumentHeader); + + // [THEN] Error about buyer electronic address is raised + AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressRequiredError(CustomerNo)); + end; + + [Test] + procedure FacturXCheckRaisesErrorWhenBuyerElectronicAddressIsMalformed() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + Customer: Record Customer; + SourceDocumentHeader: RecordRef; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X Format Check raises error when buyer electronic address does not match SIREN format + Initialize(); + + // [GIVEN] Customer "C" with malformed FR Electronic Address (non-digit prefix) + CustomerNo := CreateCustomer(''); + Customer.Get(CustomerNo); + Customer."FR Electronic Address" := 'ABCDEFGHI'; + Customer."Registration Number" := ''; + Customer.Modify(true); + + // [GIVEN] Posted sales invoice for "C" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceForCustomer(CustomerNo)); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + + // [WHEN] Factur-X Format Check is called + asserterror CheckFacturX(SourceDocumentHeader); + + // [THEN] Error about malformed buyer identifier is raised + AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressInvalidError( + Customer.FieldCaption("FR Electronic Address"), CustomerNo)); + end; + + [Test] + procedure FacturXCheckPassesAndExportsURIIDFromFrenchVAT() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + Customer: Record Customer; + TempBlob: Codeunit "Temp Blob"; + SourceDocumentHeader: RecordRef; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X uses SIREN extracted from French VAT as BuyerTradeParty URIID when FR Electronic Address and Registration Number are blank + Initialize(); + + // [GIVEN] Customer "C" with French VAT but no FR Electronic Address or Registration Number + CustomerNo := CreateCustomer(''); + Customer.Get(CustomerNo); + Customer."FR Electronic Address" := ''; + Customer."Registration Number" := ''; + Customer."VAT Registration No." := 'FR78945627890'; + Customer.Modify(true); + + // [GIVEN] Posted sales invoice for "C" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceForCustomer(CustomerNo)); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + + // [WHEN] Factur-X Format Check is called + CheckFacturX(SourceDocumentHeader); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] BuyerTradeParty/URIUniversalCommunication/URIID = '945627890' + Assert.AreEqual('945627890', + GetCIINodeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID')); + + // [THEN] schemeID = '0225' + Assert.AreEqual('0225', + GetCIIAttributeValue(TempBlob, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID'), + StrSubstNo(IncorrectValueErr, '//ram:BuyerTradeParty/ram:URIUniversalCommunication/ram:URIID/@schemeID')); + end; + + [Test] + procedure FacturXCheckRaisesErrorWhenBuyerHasOnlyNonFrenchVAT() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + Customer: Record Customer; + SourceDocumentHeader: RecordRef; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X Format Check raises error when buyer has only a non-French VAT without FR Electronic Address or Registration Number + Initialize(); + + // [GIVEN] Customer "C" with non-French VAT but no FR Electronic Address or Registration Number + CustomerNo := CreateCustomer(''); + Customer.Get(CustomerNo); + Customer."FR Electronic Address" := ''; + Customer."Registration Number" := ''; + Customer."VAT Registration No." := 'DE123456789'; + Customer.Modify(true); + + // [GIVEN] Posted sales invoice for "C" + SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceForCustomer(CustomerNo)); + SourceDocumentHeader.GetTable(SalesInvoiceHeader); + + // [WHEN] Factur-X Format Check is called + asserterror CheckFacturX(SourceDocumentHeader); + + // [THEN] Error about buyer electronic address is raised + AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressRequiredError(CustomerNo)); + end; + #endregion + + #region MultiVATRate + [Test] + procedure FacturXSalesInvoiceXMLHasMultipleVATRateTaxBreakdowns() + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + FirstVATPostingSetup: Record "VAT Posting Setup"; + SecondVATPostingSetup: Record "VAT Posting Setup"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + TempBlob: Codeunit "Temp Blob"; + CustomerNo: Code[20]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Factur-X CII XML has distinct ApplicableTradeTax groups for each VAT rate + Initialize(); + + // [GIVEN] Posted sales invoice "SI" with two lines at different VAT rates + CustomerNo := CreateCustomer(''); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + EnsureSalesInvoiceDiscountAccount(GLAccount."Gen. Bus. Posting Group", GLAccount."Gen. Prod. Posting Group"); + LibraryERM.CreateVATPostingSetupWithAccounts(FirstVATPostingSetup, FirstVATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); + GLAccount.Validate("VAT Prod. Posting Group", FirstVATPostingSetup."VAT Prod. Posting Group"); + GLAccount.Modify(true); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", FirstVATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 200); + SalesLine.Validate("Allow Invoice Disc.", true); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + LibraryERM.CreateVATPostingSetupWithAccounts(SecondVATPostingSetup, SecondVATPostingSetup."VAT Calculation Type"::"Normal VAT", 10); + SecondVATPostingSetup.Rename(Customer."VAT Bus. Posting Group", SecondVATPostingSetup."VAT Prod. Posting Group"); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("VAT Prod. Posting Group", SecondVATPostingSetup."VAT Prod. Posting Group"); + SalesLine.Validate("Unit Price", 300); + SalesLine.Validate("Allow Invoice Disc.", true); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Separate header VAT breakdowns exist for both VAT rates + Assert.AreEqual(1, GetCIINodeCount(TempBlob, + '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="20"]'), + StrSubstNo(IncorrectValueErr, '20 percent ApplicableTradeTax')); + Assert.AreEqual(1, GetCIINodeCount(TempBlob, + '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="10"]'), + StrSubstNo(IncorrectValueErr, '10 percent ApplicableTradeTax')); + Assert.AreEqual(2, GetCIINodeCount(TempBlob, + '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax'), + StrSubstNo(IncorrectValueErr, 'ApplicableTradeTax count')); + end; + + [Test] + procedure FacturXMixedVATWithInvDiscountTaxTotalEqualsBreakdownSum() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + TaxTotalAmount: Decimal; + Calculated20: Decimal; + Calculated10: Decimal; + Basis20: Decimal; + Basis10: Decimal; + ExpectedBasis20: Decimal; + ExpectedBasis10: Decimal; + ExpectedCalculated20: Decimal; + ExpectedCalculated10: Decimal; + begin + // [FEATURE] [AI test] + // [SCENARIO] Mixed VAT rates with invoice discount: TaxTotalAmount = sum of CalculatedAmounts and each breakdown matches posted line amounts + Initialize(); + + // [GIVEN] Posted sales invoice "SI" with two lines at 20% and 10% VAT and invoice discount applied + SalesInvoiceHeader.Get(CreateAndPostMultiVATInvoiceWithDiscount(true)); + GetPostedInvoiceAmountsByVATRate(SalesInvoiceHeader."No.", 20, ExpectedBasis20, ExpectedCalculated20); + GetPostedInvoiceAmountsByVATRate(SalesInvoiceHeader."No.", 10, ExpectedBasis10, ExpectedCalculated10); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Each breakdown BasisAmount equals the posted line Amount grouped by VAT rate + Basis20 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="20"]/ram:BasisAmount'); + Assert.AreEqual(ExpectedBasis20, Basis20, StrSubstNo(IncorrectValueErr, 'BasisAmount 20%')); + Basis10 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="10"]/ram:BasisAmount'); + Assert.AreEqual(ExpectedBasis10, Basis10, StrSubstNo(IncorrectValueErr, 'BasisAmount 10%')); + + // [THEN] Each breakdown CalculatedAmount equals AmountIncludingVAT - Amount per rate + Calculated20 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="20"]/ram:CalculatedAmount'); + Assert.AreEqual(ExpectedCalculated20, Calculated20, StrSubstNo(IncorrectValueErr, 'CalculatedAmount 20%')); + Calculated10 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="10"]/ram:CalculatedAmount'); + Assert.AreEqual(ExpectedCalculated10, Calculated10, StrSubstNo(IncorrectValueErr, 'CalculatedAmount 10%')); + + // [THEN] Header TaxTotalAmount equals sum of all CalculatedAmounts + TaxTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount'); + Assert.AreEqual(Calculated20 + Calculated10, TaxTotalAmount, StrSubstNo(IncorrectValueErr, 'TaxTotalAmount')); + end; + + [Test] + procedure FacturXSingleVATWithInvDiscountAllowanceAndReconciliation() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesInvoiceLine: Record "Sales Invoice Line"; + TempBlob: Codeunit "Temp Blob"; + LineTotalAmount: Decimal; + AllowanceTotalAmount: Decimal; + AllowanceAmount: Decimal; + TaxBasisTotalAmount: Decimal; + TaxTotalAmount: Decimal; + BreakdownBasis: Decimal; + BreakdownCalculated: Decimal; + ExpectedAllowanceAmount: Decimal; + ExpectedBasis: Decimal; + ExpectedCalculated: Decimal; + begin + // [FEATURE] [AI test] + // [SCENARIO] Single VAT rate with invoice discount: document allowance, breakdown basis/VAT, and BR-CO-14 reconciliation + Initialize(); + + // [GIVEN] Posted sales invoice "SI" with one line at 20% VAT and invoice discount applied + SalesInvoiceHeader.Get(CreateAndPostSingleVATInvoiceWithDiscount()); + SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + SalesInvoiceLine.FindFirst(); + ExpectedAllowanceAmount := SalesInvoiceLine."Line Amount" - SalesInvoiceLine.Amount; + ExpectedBasis := SalesInvoiceLine.Amount; + ExpectedCalculated := SalesInvoiceLine."Amount Including VAT" - SalesInvoiceLine.Amount; + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Document-level allowance equals the discount recorded on the posted line + AllowanceAmount := GetCIINodeDecimalValue( + TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge/ram:ActualAmount'); + Assert.AreEqual(ExpectedAllowanceAmount, AllowanceAmount, StrSubstNo(IncorrectValueErr, 'ActualAmount')); + + // [THEN] Breakdown BasisAmount equals posted line Amount + BreakdownBasis := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:BasisAmount'); + Assert.AreEqual(ExpectedBasis, BreakdownBasis, StrSubstNo(IncorrectValueErr, 'BasisAmount')); + + // [THEN] Breakdown CalculatedAmount equals posted VAT + BreakdownCalculated := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax/ram:CalculatedAmount'); + Assert.AreEqual(ExpectedCalculated, BreakdownCalculated, StrSubstNo(IncorrectValueErr, 'CalculatedAmount')); + + // [THEN] Monetary totals reconcile + LineTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:LineTotalAmount'); + AllowanceTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:AllowanceTotalAmount'); + TaxBasisTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount'); + Assert.AreEqual(LineTotalAmount, TaxBasisTotalAmount + AllowanceTotalAmount, + StrSubstNo(IncorrectValueErr, 'LineTotalAmount')); + + // [THEN] BR-CO-14: TaxTotalAmount equals the VAT breakdown CalculatedAmount + TaxTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount'); + Assert.AreEqual(BreakdownCalculated, TaxTotalAmount, StrSubstNo(IncorrectValueErr, 'TaxTotalAmount')); + end; + + [Test] + procedure FacturXMixedVATNoDiscountBreakdownAndReconciliation() + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + TempBlob: Codeunit "Temp Blob"; + TaxTotalAmount: Decimal; + TaxBasisTotalAmount: Decimal; + Calculated20: Decimal; + Calculated10: Decimal; + Basis20: Decimal; + Basis10: Decimal; + ExpectedBasis20: Decimal; + ExpectedBasis10: Decimal; + ExpectedCalculated20: Decimal; + ExpectedCalculated10: Decimal; + begin + // [FEATURE] [AI test] + // [SCENARIO] Mixed VAT rates without invoice discount: breakdown amounts match posted values and reconciliation holds + Initialize(); + + // [GIVEN] Posted sales invoice "SI" with two lines at 20% and 10% VAT without invoice discount + SalesInvoiceHeader.Get(CreateAndPostMultiVATInvoiceWithDiscount(false)); + GetPostedInvoiceAmountsByVATRate(SalesInvoiceHeader."No.", 20, ExpectedBasis20, ExpectedCalculated20); + GetPostedInvoiceAmountsByVATRate(SalesInvoiceHeader."No.", 10, ExpectedBasis10, ExpectedCalculated10); + + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Each breakdown BasisAmount and CalculatedAmount match posted line amounts + Basis20 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="20"]/ram:BasisAmount'); + Assert.AreEqual(ExpectedBasis20, Basis20, StrSubstNo(IncorrectValueErr, 'BasisAmount 20%')); + Basis10 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="10"]/ram:BasisAmount'); + Assert.AreEqual(ExpectedBasis10, Basis10, StrSubstNo(IncorrectValueErr, 'BasisAmount 10%')); + Calculated20 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="20"]/ram:CalculatedAmount'); + Assert.AreEqual(ExpectedCalculated20, Calculated20, StrSubstNo(IncorrectValueErr, 'CalculatedAmount 20%')); + Calculated10 := GetCIINodeDecimalValue(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:ApplicableTradeTax[ram:RateApplicablePercent="10"]/ram:CalculatedAmount'); + Assert.AreEqual(ExpectedCalculated10, Calculated10, StrSubstNo(IncorrectValueErr, 'CalculatedAmount 10%')); + + // [THEN] TaxTotalAmount equals sum of all CalculatedAmounts + TaxTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxTotalAmount'); + Assert.AreEqual(Calculated20 + Calculated10, TaxTotalAmount, StrSubstNo(IncorrectValueErr, 'TaxTotalAmount')); + + // [THEN] TaxBasisTotalAmount equals sum of all BasisAmounts (no discount) + TaxBasisTotalAmount := GetCIINodeDecimalValue(TempBlob, '//ram:SpecifiedTradeSettlementHeaderMonetarySummation/ram:TaxBasisTotalAmount'); + Assert.AreEqual(Basis20 + Basis10, TaxBasisTotalAmount, StrSubstNo(IncorrectValueErr, 'TaxBasisTotalAmount')); + + // [THEN] No document-level allowance exists + Assert.AreEqual(0, GetCIINodeCount(TempBlob, '//ram:ApplicableHeaderTradeSettlement/ram:SpecifiedTradeAllowanceCharge'), + StrSubstNo(IncorrectValueErr, 'SpecifiedTradeAllowanceCharge count')); + end; + #endregion + + local procedure AssertExpectedDialogError(ExpectedErrorText: Text) + begin + Assert.ExpectedError(ExpectedErrorText); + Assert.ExpectedErrorCode(DialogErrorCodeTok); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Factur-X CII XML Tests"); @@ -1027,6 +1812,10 @@ codeunit 148148 "Factur-X CII XML Tests" CompanyInformation.Modify(true); SetupGeneralLedger(); + CreatePostingSetupFixture(); + + LibrarySetupStorage.SaveCompanyInformation(); + LibrarySetupStorage.SaveGeneralLedgerSetup(); IsInitialized := true; Commit(); @@ -1042,6 +1831,85 @@ codeunit 148148 "Factur-X CII XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; + local procedure CreateAndPostSalesInvoiceForCustomer(CustomerNo: Code[20]): Code[20] + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + begin + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); + Customer.Modify(true); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateAndPostSalesInvoiceWithBillingModeLines(IncludeGLAccountLine: Boolean): Code[20] + var + Customer: Record Customer; + GeneralPostingSetup: Record "General Posting Setup"; + GLAccount: Record "G/L Account"; + Item: Record Item; + Location: Record Location; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + CustomerNo: Code[20]; + begin + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + Database::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + GeneralPostingSetup.Get(GLAccount."Gen. Bus. Posting Group", GLAccount."Gen. Prod. Posting Group"); + if GeneralPostingSetup."COGS Account" = '' then + GeneralPostingSetup.Validate("COGS Account", LibraryERM.CreateGLAccountNo()); + if GeneralPostingSetup."Inventory Adjmt. Account" = '' then + GeneralPostingSetup.Validate("Inventory Adjmt. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Modify(true); + CustomerNo := CreateCustomer(''); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); + Customer.Modify(true); + Item.Get(LibraryInventory.CreateItemNoWithPostingSetup( + GLAccount."Gen. Prod. Posting Group", GLAccount."VAT Prod. Posting Group")); + LibraryInventory.UpdateInventoryPostingSetup(Location, Item."Inventory Posting Group"); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + if IncludeGLAccountLine then begin + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + end; + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateAndPostSalesInvoiceWithComment(): Code[20] + var + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesDocumentWithLine("Sales Document Type"::Invoice, '')); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::" ", '', 0); + SalesLine.Validate(Description, 'Comment'); + SalesLine.Modify(true); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + local procedure CreateAndPostSalesInvoiceWithElecAddress(FRElecAddress: Text[250]): Code[20] var SalesHeader: Record "Sales Header"; @@ -1068,7 +1936,34 @@ codeunit 148148 "Factur-X CII XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; + local procedure CreateAndPostSalesCreditMemo(SalesInvoiceHeader: Record "Sales Invoice Header"): Code[20] + var + Customer: Record Customer; + GLAccount: Record "G/L Account"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + begin + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + Customer.Get(SalesInvoiceHeader."Sell-to Customer No."); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); + Customer.Modify(true); + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", Customer."No."); + SalesHeader.Validate("Applies-to Doc. Type", SalesHeader."Applies-to Doc. Type"::Invoice); + SalesHeader.Validate("Applies-to Doc. No.", SalesInvoiceHeader."No."); + SalesHeader.Modify(true); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + local procedure CreateSalesDocumentWithLine(DocType: Enum "Sales Document Type"; FRElecAddress: Text[250]): Code[20] + begin + exit(CreateSalesDocumentWithLine(DocType, FRElecAddress, '')); + end; + + local procedure CreateSalesDocumentWithLine(DocType: Enum "Sales Document Type"; FRElecAddress: Text[250]; CurrencyCode: Code[10]): Code[20] var Customer: Record Customer; GLAccount: Record "G/L Account"; @@ -1092,6 +1987,10 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, DocType, CustomerNo); + if CurrencyCode <> '' then begin + SalesHeader.Validate("Currency Code", CurrencyCode); + SalesHeader.Modify(true); + end; LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); @@ -1099,28 +1998,161 @@ codeunit 148148 "Factur-X CII XML Tests" exit(SalesHeader."No."); end; + local procedure CreateAndPostMultiVATInvoiceWithDiscount(ApplyInvoiceDiscount: Boolean): Code[20] + var + Customer: Record Customer; + CustInvoiceDisc: Record "Cust. Invoice Disc."; + GLAccount: Record "G/L Account"; + FirstVATPostingSetup: Record "VAT Posting Setup"; + SecondVATPostingSetup: Record "VAT Posting Setup"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + CustomerNo: Code[20]; + begin + CustomerNo := CreateCustomer(''); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + LibraryERM.CreateVATPostingSetupWithAccounts(FirstVATPostingSetup, FirstVATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); + GLAccount.Validate("VAT Prod. Posting Group", FirstVATPostingSetup."VAT Prod. Posting Group"); + GLAccount.Modify(true); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", FirstVATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + if ApplyInvoiceDiscount then begin + LibraryERM.CreateInvDiscForCustomer(CustInvoiceDisc, CustomerNo, '', 0); + CustInvoiceDisc.Validate("Discount %", 10); + CustInvoiceDisc.Modify(true); + end; + + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 200); + SalesLine.Validate("Allow Invoice Disc.", true); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + + LibraryERM.CreateVATPostingSetupWithAccounts(SecondVATPostingSetup, SecondVATPostingSetup."VAT Calculation Type"::"Normal VAT", 10); + SecondVATPostingSetup.Rename(Customer."VAT Bus. Posting Group", SecondVATPostingSetup."VAT Prod. Posting Group"); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("VAT Prod. Posting Group", SecondVATPostingSetup."VAT Prod. Posting Group"); + SalesLine.Validate("Unit Price", 300); + SalesLine.Validate("Allow Invoice Disc.", true); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + + if ApplyInvoiceDiscount then + LibrarySales.CalcSalesDiscount(SalesHeader); + + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure CreateAndPostSingleVATInvoiceWithDiscount(): Code[20] + var + Customer: Record Customer; + CustInvoiceDisc: Record "Cust. Invoice Disc."; + GLAccount: Record "G/L Account"; + VATPostingSetup: Record "VAT Posting Setup"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + SalesReceivablesSetup: Record "Sales & Receivables Setup"; + CustomerNo: Code[20]; + begin + CustomerNo := CreateCustomer(''); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Invoice Nos.")); + LibraryUtility.UpdateSetupNoSeriesCode( + DATABASE::"Sales & Receivables Setup", SalesReceivablesSetup.FieldNo("Posted Invoice Nos.")); + GLAccount.Get(LibraryERM.CreateGLAccountWithSalesSetup()); + EnsureSalesInvoiceDiscountAccount(GLAccount."Gen. Bus. Posting Group", GLAccount."Gen. Prod. Posting Group"); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); + GLAccount.Validate("VAT Prod. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + GLAccount.Modify(true); + Customer.Get(CustomerNo); + Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibraryERM.CreateInvDiscForCustomer(CustInvoiceDisc, CustomerNo, '', 0); + CustInvoiceDisc.Validate("Discount %", 10); + CustInvoiceDisc.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); + SalesLine.Validate("Unit Price", 500); + SalesLine.Validate("Allow Invoice Disc.", true); + SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); + SalesLine.Modify(true); + + LibrarySales.CalcSalesDiscount(SalesHeader); + exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); + end; + + local procedure EnsureSalesInvoiceDiscountAccount(GenBusPostingGroup: Code[20]; GenProdPostingGroup: Code[20]) + var + GeneralPostingSetup: Record "General Posting Setup"; + begin + GeneralPostingSetup.Get(GenBusPostingGroup, GenProdPostingGroup); + if GeneralPostingSetup."Sales Inv. Disc. Account" <> '' then + exit; + + GeneralPostingSetup.Validate("Sales Inv. Disc. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Modify(true); + end; + + local procedure GetPostedInvoiceAmountsByVATRate(DocumentNo: Code[20]; VATRate: Decimal; var BasisAmount: Decimal; var CalculatedAmount: Decimal) + var + SalesInvoiceLine: Record "Sales Invoice Line"; + begin + SalesInvoiceLine.SetRange("Document No.", DocumentNo); + SalesInvoiceLine.SetFilter(Type, '<>%1', SalesInvoiceLine.Type::" "); + SalesInvoiceLine.SetLoadFields("VAT %", Amount, "Amount Including VAT"); + if SalesInvoiceLine.FindSet() then + repeat + if SalesInvoiceLine."VAT %" = VATRate then begin + BasisAmount += SalesInvoiceLine.Amount; + CalculatedAmount += SalesInvoiceLine."Amount Including VAT" - SalesInvoiceLine.Amount; + end; + until SalesInvoiceLine.Next() = 0; + end; + local procedure CreateCustomer(FRElecAddress: Text[250]): Code[20] var Customer: Record Customer; begin LibrarySales.CreateCustomer(Customer); - if Customer."Country/Region Code" = '' then - Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); - Customer.Validate("VAT Registration No.", GetNextCustomerVATRegistrationNo()); + Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); + Customer."Registration Number" := '123456789'; Customer.Validate("FR Electronic Address", FRElecAddress); Customer.Modify(true); exit(Customer."No."); end; - local procedure GetNextCustomerVATRegistrationNo(): Text[20] + local procedure CreateCustomerWithoutIdentifiers(): Code[20] var - VATNoBody: Text[11]; - SequenceText: Text; + Customer: Record Customer; begin - CustomerVATNoSequence += 1; - SequenceText := Format(CustomerVATNoSequence); - VATNoBody := CopyStr(PadStr('', 11 - StrLen(SequenceText), '0') + SequenceText, 1, 11); - exit('FR' + VATNoBody); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); + Customer."FR Electronic Address" := ''; + Customer."FR Elec. Address Scheme" := Customer."FR Elec. Address Scheme"::" "; + Customer."VAT Registration No." := ''; + Customer."Registration Number" := ''; + Customer.Modify(true); + exit(Customer."No."); + end; + + local procedure CheckFacturX(var SourceDocumentHeader: RecordRef) + var + EDocumentService: Record "E-Document Service"; + begin + FacturXFormat.Check(SourceDocumentHeader, EDocumentService, "E-Document Processing Phase"::Create); end; local procedure CreateSalesInvoiceCIIXML(var TempBlob: Codeunit "Temp Blob") @@ -1238,11 +2270,37 @@ codeunit 148148 "Factur-X CII XML Tests" exit(''); end; + local procedure GetCIINodeCount(var TempBlob: Codeunit "Temp Blob"; XPath: Text): Integer + var + XmlDoc: XmlDocument; + NamespaceMgr: XmlNamespaceManager; + Nodes: XmlNodeList; + InStr: InStream; + begin + TempBlob.CreateInStream(InStr, TextEncoding::UTF8); + XmlDocument.ReadFrom(InStr, XmlDoc); + BuildNamespaceManager(XmlDoc, NamespaceMgr); + + XmlDoc.SelectNodes(XPath, NamespaceMgr, Nodes); + exit(Nodes.Count()); + end; + + local procedure GetCIINodeDecimalValue(var TempBlob: Codeunit "Temp Blob"; XPath: Text): Decimal + var + NodeText: Text; + Result: Decimal; + begin + NodeText := GetCIINodeValue(TempBlob, XPath); + Evaluate(Result, NodeText, 9); + exit(Result); + end; + local procedure BuildNamespaceManager(XmlDoc: XmlDocument; var NamespaceMgr: XmlNamespaceManager) begin NamespaceMgr.NameTable(XmlDoc.NameTable()); NamespaceMgr.AddNamespace('rsm', 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100'); NamespaceMgr.AddNamespace('ram', 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100'); + NamespaceMgr.AddNamespace('qdt', 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100'); NamespaceMgr.AddNamespace('udt', 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100'); end; @@ -1257,6 +2315,69 @@ codeunit 148148 "Factur-X CII XML Tests" end; end; + local procedure CreatePostingSetupFixture() + var + GenBusinessPostingGroup: Record "Gen. Business Posting Group"; + GenProductPostingGroup: Record "Gen. Product Posting Group"; + GeneralPostingSetup: Record "General Posting Setup"; + VATBusinessPostingGroup: Record "VAT Business Posting Group"; + VATProductPostingGroup: Record "VAT Product Posting Group"; + VATPostingSetup: Record "VAT Posting Setup"; + PostingGroupCode: Code[20]; + begin + PostingGroupCode := '0FRFACTURX'; + + if VATPostingSetup.Get(PostingGroupCode, PostingGroupCode) then + exit; + + if not GenBusinessPostingGroup.Get(PostingGroupCode) then begin + GenBusinessPostingGroup.Code := PostingGroupCode; + GenBusinessPostingGroup.Insert(true); + end; + if not GenProductPostingGroup.Get(PostingGroupCode) then begin + GenProductPostingGroup.Code := PostingGroupCode; + GenProductPostingGroup.Insert(true); + end; + + if not GeneralPostingSetup.Get(PostingGroupCode, PostingGroupCode) then begin + GeneralPostingSetup."Gen. Bus. Posting Group" := PostingGroupCode; + GeneralPostingSetup."Gen. Prod. Posting Group" := PostingGroupCode; + GeneralPostingSetup.Insert(true); + end; + GeneralPostingSetup.Validate("Sales Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Sales Credit Memo Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Sales Prepayments Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purch. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purch. Credit Memo Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purch. Prepayments Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("COGS Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("COGS Account (Interim)", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Inventory Adjmt. Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Direct Cost Applied Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Overhead Applied Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Validate("Purchase Variance Account", LibraryERM.CreateGLAccountNo()); + GeneralPostingSetup.Modify(true); + + if not VATBusinessPostingGroup.Get(PostingGroupCode) then begin + VATBusinessPostingGroup.Code := PostingGroupCode; + VATBusinessPostingGroup.Insert(true); + end; + if not VATProductPostingGroup.Get(PostingGroupCode) then begin + VATProductPostingGroup.Code := PostingGroupCode; + VATProductPostingGroup.Insert(true); + end; + + VATPostingSetup."VAT Bus. Posting Group" := PostingGroupCode; + VATPostingSetup."VAT Prod. Posting Group" := PostingGroupCode; + VATPostingSetup.Insert(true); + VATPostingSetup.Validate("VAT Calculation Type", VATPostingSetup."VAT Calculation Type"::"Normal VAT"); + VATPostingSetup.Validate("VAT %", 20); + VATPostingSetup.Validate("Tax Category", 'S'); + VATPostingSetup.Validate("Sales VAT Account", LibraryERM.CreateGLAccountNo()); + VATPostingSetup.Validate("Purchase VAT Account", LibraryERM.CreateGLAccountNo()); + VATPostingSetup.Modify(true); + end; + local procedure GetUnitOfMeasureCode(): Code[10] var UnitOfMeasure: Record "Unit of Measure"; diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index dc5fa50a5c5..bf976ecd725 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -56,6 +56,8 @@ permissionset 6100 "E-Doc. Core - Objects" table "ED Purchase Line Field Setup" = X, table "E-Doc Sample Purch. Inv File" = X, table "E-Document Message" = X, + table "E-Doc. Payment Occurrence" = X, + table "E-Doc. External Reference" = X, #if not CLEAN28 #pragma warning disable AL0432 table "EDoc Historical Matching Setup" = X, @@ -104,9 +106,12 @@ permissionset 6100 "E-Doc. Core - Objects" #endif codeunit "E-Doc. Attachment Processor" = X, codeunit "E-Doc. Hist. Line Data Loader" = X, + codeunit "E-Document Message API" = X, codeunit "E-Doc. Message Context" = X, codeunit "E-Doc. Message Mgt." = X, + codeunit "E-Doc. Message Send Job" = X, codeunit "E-Doc. Msg. Transport Default" = X, + codeunit "E-Doc. Payment Occurrence Mgt." = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, page "E-Doc. Changes Preview" = X, diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al index 9273943723d..db121936686 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al @@ -36,6 +36,8 @@ permissionset 6101 "E-Doc. Core - Read" tabledata "E-Document Service Status" = R, tabledata "E-Document Integration Log" = R, tabledata "E-Document Message" = R, + tabledata "E-Doc. Payment Occurrence" = R, + tabledata "E-Doc. External Reference" = R, #endregion Logging tabledata "E-Doc. Imported Line" = R, tabledata "E-Doc. Order Match" = R, diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al index 604fba02221..6cc4d4dc41f 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al @@ -42,6 +42,8 @@ permissionset 6105 "E-Doc. Core - User" tabledata "E-Doc. Data Storage" = imd, tabledata "E-Document Integration Log" = imd, tabledata "E-Document Message" = imd, + tabledata "E-Doc. Payment Occurrence" = imd, + tabledata "E-Doc. External Reference" = imd, #endregion Logging tabledata "E-Doc. Imported Line" = IMD, tabledata "E-Doc. Order Match" = IMD, diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al index 0d58c12091a..3f446ebd4a0 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al @@ -7,7 +7,17 @@ namespace Microsoft.eServices.EDocument.Integration.Interfaces; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Message; +/// +/// Sends a child message associated with an E-Document through a service integration. +/// interface IMessageSender { + /// + /// Sends the payload in the message context. + /// + /// The parent E-Document. + /// The service used to send the message. + /// The message payload, transport state, and result. The implementation must set the result to Sent after successful transmission. + /// The implementation is responsible for obtaining any privacy consent required by the external service before transmitting data. procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"); } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al index 3049d5902d7..f84d85f9812 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al @@ -14,6 +14,7 @@ enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, I Access = Public; DefaultImplementation = IConsentManager = "Consent Manager Default Impl.", IMessageSender = "E-Doc. Msg. Transport Default"; + UnknownValueImplementation = IMessageSender = "E-Doc. Msg. Transport Default"; value(0; "No Integration") { diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index c00103cff7c..0ffe24b98c7 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; using System.Telemetry; using System.Threading; @@ -21,6 +22,11 @@ codeunit 6133 "E-Document Background Jobs" EDocument.Modify(); end; + procedure ScheduleMessageSend(EDocumentMessage: Record "E-Document Message") + begin + ScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0); + end; + procedure ScheduleGetResponseJob() begin ScheduleGetResponseJob(true); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al new file mode 100644 index 00000000000..d2ea41a0d11 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument; + +table 6434 "E-Doc. External Reference" +{ + Access = Internal; + Caption = 'E-Document External Reference'; + DataClassification = CustomerContent; + InherentEntitlements = RIMDX; + InherentPermissions = RIMDX; + ReplicateData = false; + + fields + { + field(1; "Entry No."; Integer) + { + AutoIncrement = true; + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + } + field(2; Service; Code[20]) + { + Caption = 'Service'; + DataClassification = SystemMetadata; + TableRelation = "E-Document Service"; + } + field(3; "External Document ID"; Text[250]) + { + Caption = 'External Document ID'; + DataClassification = CustomerContent; + } + field(4; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Document"."Entry No"; + } + field(5; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + key(ExternalDocument; Service, "External Document ID") + { + Unique = true; + } + } +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al index 3d052c68d72..a6f93a5f472 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al @@ -22,31 +22,55 @@ codeunit 6533 "E-Doc. Message Context" Payload := TempBlob; end; + /// + /// Gets the entry number of the child E-Document message being sent. + /// + /// The E-Document message entry number. procedure GetMessageEntryNo(): Integer begin exit(MessageEntryNo); end; + /// + /// Gets the semantic type of the child E-Document message. + /// + /// The E-Document message type. procedure GetMessageType(): Enum "E-Document Message Type" begin exit(MessageType); end; + /// + /// Gets the response type represented by the child message. + /// + /// The E-Document response type. procedure GetResponseType(): Enum "E-Doc. Response Type" begin exit(ResponseType); end; + /// + /// Gets the message payload. + /// + /// A temporary blob containing the message payload. procedure GetTempBlob(): Codeunit "Temp Blob" begin exit(Payload); end; + /// + /// Gets the HTTP state used to record the connector request and response. + /// + /// The HTTP message state. procedure Http(): Codeunit "Http Message State" begin exit(HttpMessageState); end; + /// + /// Gets the transport result. A connector must set the status to Sent after successful transmission. + /// + /// The integration action status. procedure Status(): Codeunit "Integration Action Status" begin exit(IntegrationActionStatus); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 915431b9d44..dac5d2a0222 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -92,7 +92,8 @@ codeunit 6433 "E-Doc. Message Mgt." begin EDocMessage.Get(MessageEntryNo); EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); - EDocMessage.TestField(Status, EDocMessage.Status::Created); + if not (EDocMessage.Status in [EDocMessage.Status::Created, EDocMessage.Status::Queued, EDocMessage.Status::Error]) then + EDocMessage.FieldError(Status); EDocMessage.TestField(Service); EDocument.Get(EDocMessage."E-Document Entry No."); @@ -102,7 +103,6 @@ codeunit 6433 "E-Doc. Message Mgt." Error(MessagePayloadErr, MessageEntryNo); EDocMessageContext.Initialize(EDocMessage, TempBlob); - EDocMessageContext.Status().SetStatus("E-Document Service Status"::Sent); MessageSender := EDocumentService."Service Integration V2"; MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then @@ -111,7 +111,89 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); EDocMessage.Status := EDocMessage.Status::Sent; + EDocMessage."Last Attempt At" := CurrentDateTime(); + Clear(EDocMessage."Last Error"); + EDocMessage.Modify(); + end; + + procedure QueueMessage(MessageEntryNo: Integer) + var + EDocMessage: Record "E-Document Message"; + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; + begin + EDocMessage.Get(MessageEntryNo); + EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); + EDocMessage.TestField(Status, EDocMessage.Status::Created); + EDocMessage.TestField(Service); + + EDocMessage.Status := EDocMessage.Status::Queued; + EDocMessage.Modify(); + EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); + end; + + procedure RegisterExternalDocumentReference(EDocument: Record "E-Document"; ServiceCode: Code[20]; ExternalDocumentID: Text[250]) + var + EDocExternalReference: Record "E-Doc. External Reference"; + EDocumentService: Record "E-Document Service"; + begin + EDocument.Get(EDocument."Entry No"); + EDocument.TestField(Service, ServiceCode); + EDocumentService.Get(ServiceCode); + if ExternalDocumentID = '' then + Error(ExternalDocumentIDRequiredErr); + + EDocExternalReference.SetRange(Service, ServiceCode); + EDocExternalReference.SetRange("External Document ID", ExternalDocumentID); + if EDocExternalReference.FindFirst() then begin + if EDocExternalReference."E-Document Entry No." = EDocument."Entry No" then + exit; + Error(ExternalDocumentIDConflictErr, ExternalDocumentID, ServiceCode); + end; + + EDocExternalReference.Init(); + EDocExternalReference.Service := ServiceCode; + EDocExternalReference."External Document ID" := ExternalDocumentID; + EDocExternalReference."E-Document Entry No." := EDocument."Entry No"; + EDocExternalReference."Created At" := CurrentDateTime(); + EDocExternalReference.Insert(); + end; + + procedure CreateIncomingMessage(ServiceCode: Code[20]; ExternalDocumentID: Text[250]; ExternalMessageID: Text[250]; MessageType: Enum "E-Document Message Type"; ResponseType: Enum "E-Doc. Response Type"; ReceivedAt: DateTime; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocument: Record "E-Document"; + EDocExternalReference: Record "E-Doc. External Reference"; + EDocMessage: Record "E-Document Message"; + MessageEntryNo: Integer; + begin + if ExternalDocumentID = '' then + Error(ExternalDocumentIDRequiredErr); + if ExternalMessageID = '' then + Error(ExternalMessageIDRequiredErr); + if not TempBlob.HasValue() then + Error(IncomingMessagePayloadRequiredErr); + + EDocMessage.SetRange(Service, ServiceCode); + EDocMessage.SetRange("External Message ID", ExternalMessageID); + if EDocMessage.FindFirst() then + exit(EDocMessage."Entry No."); + + EDocExternalReference.SetRange(Service, ServiceCode); + EDocExternalReference.SetRange("External Document ID", ExternalDocumentID); + if not EDocExternalReference.FindFirst() then + Error(ExternalDocumentNotFoundErr, ExternalDocumentID, ServiceCode); + + EDocument.Get(EDocExternalReference."E-Document Entry No."); + MessageEntryNo := CreateMessage(EDocument, MessageType, "E-Document Direction"::Incoming, ResponseType, TempBlob); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::Received; + EDocMessage."External Message ID" := ExternalMessageID; + EDocMessage."External Document ID" := ExternalDocumentID; + if ReceivedAt = 0DT then + EDocMessage."Received At" := CurrentDateTime() + else + EDocMessage."Received At" := ReceivedAt; EDocMessage.Modify(); + exit(MessageEntryNo); end; local procedure InsertDataStorage(TempBlob: Codeunit "Temp Blob"): Integer @@ -135,4 +217,9 @@ codeunit 6433 "E-Doc. Message Mgt." var MessagePayloadErr: Label 'E-Document message %1 does not contain a payload.', Comment = '%1 = E-Document message entry number'; MessageSendingErr: Label 'E-Document message %1 could not be sent. The integration returned status %2.', Comment = '%1 = E-Document message entry number, %2 = integration status'; + ExternalDocumentIDRequiredErr: Label 'An external document ID is required.'; + ExternalMessageIDRequiredErr: Label 'An external message ID is required.'; + IncomingMessagePayloadRequiredErr: Label 'An incoming E-Document message payload is required.'; + ExternalDocumentIDConflictErr: Label 'External document ID %1 is already associated with another E-Document for service %2.', Comment = '%1 = external document ID, %2 = service code'; + ExternalDocumentNotFoundErr: Label 'External document ID %1 is not registered for E-Document service %2.', Comment = '%1 = external document ID, %2 = service code'; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al new file mode 100644 index 00000000000..8d43c26cb34 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using System.Threading; + +codeunit 6535 "E-Doc. Message Send Job" +{ + Access = Internal; + TableNo = "Job Queue Entry"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + EDocumentMessage: Record "E-Document Message"; + LastErrorText: Text; + begin + EDocumentMessage.Get(Rec."Record ID to Process"); + if TrySendMessage(EDocumentMessage."Entry No.") then + exit; + + LastErrorText := GetLastErrorText(); + EDocumentMessage.Get(EDocumentMessage."Entry No."); + EDocumentMessage.Status := EDocumentMessage.Status::Error; + EDocumentMessage."Last Attempt At" := CurrentDateTime(); + EDocumentMessage."Retry Count" += 1; + EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); + EDocumentMessage.Modify(); + Commit(); + Error(MessageSendFailedErr, EDocumentMessage."Entry No.", LastErrorText); + end; + + [TryFunction] + local procedure TrySendMessage(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.SendMessage(MessageEntryNo); + end; + + var + MessageSendFailedErr: Label 'E-Document message %1 could not be sent. %2', Comment = '%1 = message entry number, %2 = connector error'; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al index 62646edc3ba..fdca39dd3b8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al @@ -19,4 +19,16 @@ enum 6429 "E-Doc. Message Status" { Caption = 'Sent'; } + value(2; Queued) + { + Caption = 'Queued'; + } + value(3; Error) + { + Caption = 'Error'; + } + value(4; Received) + { + Caption = 'Received'; + } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al new file mode 100644 index 00000000000..7a143a77010 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al @@ -0,0 +1,94 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument; + +/// +/// Stores an immutable payment application or reversal associated with an outgoing E-Document. +/// +table 6433 "E-Doc. Payment Occurrence" +{ + Access = Public; + Caption = 'E-Document Payment Occurrence'; + DataClassification = CustomerContent; + InherentEntitlements = RIMDX; + InherentPermissions = RIMDX; + ReplicateData = false; + + fields + { + field(1; "Entry No."; Integer) + { + AutoIncrement = true; + Caption = 'Entry No.'; + DataClassification = SystemMetadata; + } + field(2; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Document"."Entry No"; + } + field(3; Type; Enum "E-Doc. Payment Occurrence Type") + { + Caption = 'Type'; + DataClassification = SystemMetadata; + } + field(4; "Source Occurrence ID"; Guid) + { + Caption = 'Source Occurrence ID'; + DataClassification = SystemMetadata; + } + field(5; "Original Occurrence Entry No."; Integer) + { + Caption = 'Original Occurrence Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "E-Doc. Payment Occurrence"."Entry No."; + } + field(6; Amount; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Amount'; + DataClassification = CustomerContent; + } + field(7; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + } + field(8; "Event Date"; Date) + { + Caption = 'Event Date'; + DataClassification = CustomerContent; + } + field(9; "Detailed Ledger Entry No."; Integer) + { + Caption = 'Detailed Ledger Entry No.'; + DataClassification = SystemMetadata; + } + field(10; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + key(Occurrence; "E-Document Entry No.", "Source Occurrence ID", Type) + { + Unique = true; + } + key(Source; "Source Occurrence ID", Type) + { + } + } +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al new file mode 100644 index 00000000000..2114fec8103 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -0,0 +1,154 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using Microsoft.eServices.EDocument; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Posting; +using Microsoft.Finance.ReceivablesPayables; +using Microsoft.Sales.Customer; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; + +/// +/// Captures payment applications and reversals for outgoing E-Documents and publishes them to localization apps. +/// +codeunit 6536 "E-Doc. Payment Occurrence Mgt." +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + Permissions = + tabledata "Cust. Ledger Entry" = r, + tabledata "Detailed Cust. Ledg. Entry" = r, + tabledata "E-Document" = r, + tabledata "E-Doc. Payment Occurrence" = rim, + tabledata "Sales Invoice Header" = r; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) + begin + ProcessApplication(DtldCustLedgEntry); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] + local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); + end; + + /// + /// Captures an invoice payment application for every matching outgoing E-Document. + /// + /// The detailed customer ledger application entry. + procedure ProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + EDocument: Record "E-Document"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + begin + if not IsInvoiceApplication(DetailedCustLedgEntry) then + exit; + if not InvoiceCustLedgerEntry.Get(DetailedCustLedgEntry."Cust. Ledger Entry No.") then + exit; + if not PaymentCustLedgerEntry.Get(DetailedCustLedgEntry."Applied Cust. Ledger Entry No.") then + exit; + if PaymentCustLedgerEntry."Document Type" <> PaymentCustLedgerEntry."Document Type"::Payment then + exit; + if not FindInvoiceEDocuments(EDocument, InvoiceCustLedgerEntry) then + exit; + + repeat + CreateOccurrence( + EDocument, "E-Doc. Payment Occurrence Type"::Applied, DetailedCustLedgEntry.SystemId, + -DetailedCustLedgEntry.Amount, DetailedCustLedgEntry."Currency Code", DetailedCustLedgEntry."Posting Date", + DetailedCustLedgEntry."Entry No.", 0); + until EDocument.Next() = 0; + end; + + /// + /// Captures the reversal of each payment occurrence created from the original application entry. + /// + /// The original detailed customer ledger application entry. + /// The detailed customer ledger reversal entry. + procedure ProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + AppliedOccurrence: Record "E-Doc. Payment Occurrence"; + EDocument: Record "E-Document"; + begin + if not IsInvoiceApplication(OldDetailedCustLedgEntry) then + exit; + + AppliedOccurrence.SetRange("Source Occurrence ID", OldDetailedCustLedgEntry.SystemId); + AppliedOccurrence.SetRange(Type, AppliedOccurrence.Type::Applied); + if not AppliedOccurrence.FindSet() then + exit; + + repeat + EDocument.Get(AppliedOccurrence."E-Document Entry No."); + CreateOccurrence( + EDocument, "E-Doc. Payment Occurrence Type"::Reversed, NewDetailedCustLedgEntry.SystemId, + -AppliedOccurrence.Amount, AppliedOccurrence."Currency Code", NewDetailedCustLedgEntry."Posting Date", + NewDetailedCustLedgEntry."Entry No.", AppliedOccurrence."Entry No."); + until AppliedOccurrence.Next() = 0; + end; + + local procedure CreateOccurrence(EDocument: Record "E-Document"; OccurrenceType: Enum "E-Doc. Payment Occurrence Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) + var + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + begin + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange("Source Occurrence ID", SourceOccurrenceID); + EDocPaymentOccurrence.SetRange(Type, OccurrenceType); + if not EDocPaymentOccurrence.IsEmpty() then + exit; + + EDocPaymentOccurrence.Init(); + EDocPaymentOccurrence."E-Document Entry No." := EDocument."Entry No"; + EDocPaymentOccurrence.Type := OccurrenceType; + EDocPaymentOccurrence."Source Occurrence ID" := SourceOccurrenceID; + EDocPaymentOccurrence."Original Occurrence Entry No." := OriginalOccurrenceEntryNo; + EDocPaymentOccurrence.Amount := Amount; + EDocPaymentOccurrence."Currency Code" := CurrencyCode; + EDocPaymentOccurrence."Event Date" := EventDate; + EDocPaymentOccurrence."Detailed Ledger Entry No." := DetailedLedgerEntryNo; + EDocPaymentOccurrence."Created At" := CurrentDateTime(); + EDocPaymentOccurrence.Insert(); + OnAfterCreatePaymentOccurrence(EDocPaymentOccurrence); + end; + + local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + if InvoiceCustLedgerEntry."Document Type" <> InvoiceCustLedgerEntry."Document Type"::Invoice then + exit(false); + if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then + exit(false); + + EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); + EDocument.SetRange(Direction, EDocument.Direction::Outgoing); + EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); + exit(EDocument.FindSet()); + end; + + local procedure IsInvoiceApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean + begin + exit( + (DetailedCustLedgEntry."Entry Type" = DetailedCustLedgEntry."Entry Type"::Application) and + (DetailedCustLedgEntry."Initial Document Type" = DetailedCustLedgEntry."Initial Document Type"::Invoice) and + (DetailedCustLedgEntry.Amount < 0)); + end; + + /// + /// Notifies localization and format apps after a payment occurrence has been persisted. + /// + /// The persisted payment occurrence. + [IntegrationEvent(false, false)] + procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al new file mode 100644 index 00000000000..0800eda61c8 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al @@ -0,0 +1,23 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +/// +/// Identifies whether an E-Document payment occurrence applies or reverses an amount. +/// +enum 6430 "E-Doc. Payment Occurrence Type" +{ + Access = Public; + Extensible = false; + + value(0; Applied) + { + Caption = 'Applied'; + } + value(1; Reversed) + { + Caption = 'Reversed'; + } +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al index 3c7186aa371..e8381145d14 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al @@ -69,6 +69,36 @@ table 6432 "E-Document Message" TableRelation = "E-Document Service"; DataClassification = SystemMetadata; } + field(10; "Last Attempt At"; DateTime) + { + Caption = 'Last Attempt At'; + DataClassification = SystemMetadata; + } + field(11; "Retry Count"; Integer) + { + Caption = 'Retry Count'; + DataClassification = SystemMetadata; + } + field(12; "Last Error"; Text[2048]) + { + Caption = 'Last Error'; + DataClassification = CustomerContent; + } + field(13; "External Message ID"; Text[250]) + { + Caption = 'External Message ID'; + DataClassification = CustomerContent; + } + field(14; "External Document ID"; Text[250]) + { + Caption = 'External Document ID'; + DataClassification = CustomerContent; + } + field(15; "Received At"; DateTime) + { + Caption = 'Received At'; + DataClassification = SystemMetadata; + } } keys @@ -80,5 +110,8 @@ table 6432 "E-Document Message" key(EDocument; "E-Document Entry No.") { } + key(ExternalMessage; Service, "External Message ID") + { + } } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al index 17ca7915849..06b7174d684 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -12,6 +12,14 @@ codeunit 6532 "E-Document Message API" Access = Public; InherentEntitlements = X; + /// + /// Creates an outgoing child message for an E-Document and stores its payload. + /// + /// The parent E-Document. + /// The semantic message type. + /// The response represented by the message. + /// The message payload. + /// The entry number of the created E-Document message. procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob"): Integer var EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; @@ -19,10 +27,56 @@ codeunit 6532 "E-Document Message API" exit(EDocMessageMgt.CreateMessage(EDocument, MessageType, EDocument.Direction::Outgoing, ResponseType, TempBlob)); end; + /// + /// Sends a previously created outgoing E-Document message through its E-Document service. + /// + /// The entry number of the E-Document message to send. procedure SendMessage(MessageEntryNo: Integer) var EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; begin EDocMessageMgt.SendMessage(MessageEntryNo); end; + + /// + /// Queues a previously created outgoing E-Document message for background transmission. + /// + /// The entry number of the E-Document message to queue. + procedure QueueMessage(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.QueueMessage(MessageEntryNo); + end; + + /// + /// Associates an external service document identifier with an E-Document for later message correlation. + /// + /// The E-Document known by the external service. + /// The E-Document service that issued the identifier. + /// The service-specific document identifier. + procedure RegisterExternalDocumentReference(EDocument: Record "E-Document"; ServiceCode: Code[20]; ExternalDocumentID: Text[250]) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.RegisterExternalDocumentReference(EDocument, ServiceCode, ExternalDocumentID); + end; + + /// + /// Stores an incoming child message and correlates it to an E-Document by service-specific identifiers. + /// + /// The service from which the message was received. + /// The external identifier of the parent document. + /// The external identifier used to deduplicate the message. + /// The semantic message type. + /// The response represented by the message. + /// The source timestamp, or zero to use the current date and time. + /// The original message payload. + /// The entry number of the new or previously stored E-Document message. + procedure CreateIncomingMessage(ServiceCode: Code[20]; ExternalDocumentID: Text[250]; ExternalMessageID: Text[250]; MessageType: Enum "E-Document Message Type"; ResponseType: Enum "E-Doc. Response Type"; ReceivedAt: DateTime; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.CreateIncomingMessage(ServiceCode, ExternalDocumentID, ExternalMessageID, MessageType, ResponseType, ReceivedAt, TempBlob)); + end; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al index a44e49516c1..3a3ef8576a8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al @@ -53,6 +53,31 @@ page 6434 "E-Document Messages FactBox" ApplicationArea = Basic, Suite; ToolTip = 'Specifies when the message was created.'; } + field("Last Attempt At"; Rec."Last Attempt At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the service last attempted to send the message.'; + } + field("Retry Count"; Rec."Retry Count") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies how many background send attempts have failed.'; + } + field("Last Error"; Rec."Last Error") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the error returned by the most recent failed send attempt.'; + } + field("External Message ID"; Rec."External Message ID") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the identifier assigned to the message by the external service.'; + } + field("Received At"; Rec."Received At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the external service created or delivered the incoming message.'; + } } } } From 161edf48683a08c14cfb6fa1efae123df238dd84 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 16:35:00 +0200 Subject: [PATCH 04/49] new changes --- .../app/src/Core/FREInvoiceMessage.Table.al | 13 + .../src/Core/FREInvoiceMessageAPI.Codeunit.al | 154 ++++++++ .../Core/FREInvoiceMessageBuilder.Codeunit.al | 65 +++- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 46 ++- .../app/src/Core/FREInvoiceMessages.Page.al | 10 + .../EReportingEDocuments.PageExt.al | 16 + .../Extensions/FREDocResponseType.EnumExt.al | 4 + .../src/EDocFRStructImportTests.Codeunit.al | 8 + ...nit.al => FREDocMsgSenderMock.Codeunit.al} | 0 .../src/FREInvoiceMessageTests.Codeunit.al | 360 +++++++++++++++++- .../test/src/FacturXCIIXMLTests.Codeunit.al | 59 ++- .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 4 +- .../Message/EDocMessageMgt.Codeunit.al | 32 ++ ...al => EDocMsgTransportDefault.Codeunit.al} | 0 .../Message/EDocPaymentOccurrenceType.Enum.al | 2 +- .../Message/EDocumentMessageAPI.Codeunit.al | 48 +++ .../EDocRemitAdviceExport.Codeunit.al | 2 +- .../EDocRemitAdviceJournal.ReportExt.al | 8 +- 18 files changed, 757 insertions(+), 74 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al rename src/Apps/FR/EDocument_FR/EReportingFR/test/src/{FREDocMessageSenderMock.Codeunit.al => FREDocMsgSenderMock.Codeunit.al} (100%) rename src/Apps/W1/EDocument/App/src/Processing/Message/{EDocMessageTransportDefault.Codeunit.al => EDocMsgTransportDefault.Codeunit.al} (100%) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 3c0058c7fbf..64911991e0c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -86,6 +86,16 @@ table 10970 "FR E-Invoice Message" Caption = 'Created At'; DataClassification = SystemMetadata; } + field(14; "External Message ID"; Text[250]) + { + Caption = 'External Message ID'; + DataClassification = CustomerContent; + } + field(15; "Received At"; DateTime) + { + Caption = 'Received At'; + DataClassification = SystemMetadata; + } } keys @@ -101,5 +111,8 @@ table 10970 "FR E-Invoice Message" key(DetailedLedgerEntry; Type, "Detailed Ledger Entry No.") { } + key(EDocumentMessage; "E-Document Message Entry No.") + { + } } } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al new file mode 100644 index 00000000000..38aaa194bd6 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al @@ -0,0 +1,154 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; +using System.Utilities; + +codeunit 10987 "FR E-Invoice Message API" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Receives, validates, and stores a French invoice lifecycle message from an E-Document service. + /// + /// The E-Document service that received the message. + /// The service-specific identifier registered for the parent E-Document. + /// The service-specific message identifier used for deduplication. + /// The source timestamp, or zero to use the current date and time. + /// The original lifecycle XML payload. + /// The entry number of the normalized French invoice message. + procedure ReceiveMessage(ServiceCode: Code[20]; ExternalDocumentID: Text[250]; ExternalMessageID: Text[250]; ReceivedAt: DateTime; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + MessageType: Enum "FR E-Invoice Message Type"; + ResponseType: Enum "E-Doc. Response Type"; + InvoiceID: Text; + ReasonCode: Text; + ReasonDescription: Text; + MessageEntryNo: Integer; + begin + ParseMessage(TempBlob, InvoiceID, MessageType, ReasonCode, ReasonDescription); + ResponseType := GetResponseType(MessageType); + MessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( + ServiceCode, ExternalDocumentID, ExternalMessageID, "E-Document Message Type"::"FR Invoice Lifecycle", + ResponseType, ReceivedAt, TempBlob); + + FREInvoiceMessage.SetRange("E-Document Message Entry No.", MessageEntryNo); + if FREInvoiceMessage.FindFirst() then + exit(FREInvoiceMessage."Entry No."); + + EDocumentMessageAPI.GetMessageEDocument(MessageEntryNo, EDocument); + EDocument.TestField(Direction, EDocument.Direction::Outgoing); + if EDocument."Document No." <> InvoiceID then + Error(InvoiceMismatchErr, InvoiceID, EDocument."Document No."); + + FREInvoiceMessage.Init(); + FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; + FREInvoiceMessage.Type := MessageType; + FREInvoiceMessage."Source Occurrence ID" := CreateGuid(); + FREInvoiceMessage."Reason Code" := CopyStr(ReasonCode, 1, MaxStrLen(FREInvoiceMessage."Reason Code")); + FREInvoiceMessage."Reason Description" := CopyStr(ReasonDescription, 1, MaxStrLen(FREInvoiceMessage."Reason Description")); + FREInvoiceMessage."E-Document Message Entry No." := MessageEntryNo; + FREInvoiceMessage."External Message ID" := ExternalMessageID; + if ReceivedAt = 0DT then + FREInvoiceMessage."Received At" := CurrentDateTime() + else + FREInvoiceMessage."Received At" := ReceivedAt; + FREInvoiceMessage."Event Date" := DT2Date(FREInvoiceMessage."Received At"); + FREInvoiceMessage."Created At" := CurrentDateTime(); + FREInvoiceMessage.Insert(); + exit(FREInvoiceMessage."Entry No."); + end; + + local procedure ParseMessage(TempBlob: Codeunit "Temp Blob"; var InvoiceID: Text; var MessageType: Enum "FR E-Invoice Message Type"; var ReasonCode: Text; var ReasonDescription: Text) + var + XmlDoc: XmlDocument; + InStream: InStream; + StatusText: Text; + begin + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + if not XmlDocument.ReadFrom(InStream, XmlDoc) then + Error(InvalidXmlErr); + + InvoiceID := GetRequiredNodeText(XmlDoc, '//*[local-name()="InvoiceID" or local-name()="IssuerAssignedID"]', InvoiceIDErr); + StatusText := GetRequiredNodeText(XmlDoc, '//*[local-name()="ProcessConditionCode" or local-name()="ProcessCondition" or local-name()="Status"]', StatusErr); + MessageType := MapMessageType(StatusText); + ReasonCode := GetOptionalNodeText(XmlDoc, '//*[local-name()="ReasonCode"]'); + ReasonDescription := GetOptionalNodeText(XmlDoc, '//*[local-name()="Reason" or local-name()="ReasonDescription"]'); + + if MessageType = MessageType::"Technical Rejected" then begin + if ReasonCode = '' then + Error(RejectedReasonCodeErr); + if ReasonDescription = '' then + Error(RejectedReasonDescriptionErr); + end; + end; + + local procedure MapMessageType(StatusText: Text): Enum "FR E-Invoice Message Type" + var + MessageType: Enum "FR E-Invoice Message Type"; + begin + case UpperCase(StatusText.Trim()) of + '200', 'SUBMITTED', 'DÉPOSÉE', 'DEPOSEE': + exit(MessageType::Submitted); + '205', 'ACCEPTED', 'ACCEPTÉE', 'ACCEPTEE', 'APPROUVÉE', 'APPROUVEE': + exit(MessageType::Accepted); + 'REJECTED', 'TECHNICAL REJECTED', 'REJETÉE', 'REJETEE': + exit(MessageType::"Technical Rejected"); + '210', 'REFUSED', 'REFUSÉE', 'REFUSEE': + exit(MessageType::Refused); + else + Error(UnsupportedStatusErr, StatusText); + end; + end; + + local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type" + begin + case MessageType of + MessageType::Submitted: + exit("E-Doc. Response Type"::Submitted); + MessageType::Accepted: + exit("E-Doc. Response Type"::Accepted); + MessageType::"Technical Rejected": + exit("E-Doc. Response Type"::Rejected); + MessageType::Refused: + exit("E-Doc. Response Type"::Refused); + else + Error(UnsupportedStatusErr, Format(MessageType)); + end; + end; + + local procedure GetRequiredNodeText(XmlDoc: XmlDocument; XPath: Text; ErrorText: Text): Text + var + XmlNode: XmlNode; + begin + if not XmlDoc.SelectSingleNode(XPath, XmlNode) then + Error(ErrorText); + exit(XmlNode.AsXmlElement().InnerText()); + end; + + local procedure GetOptionalNodeText(XmlDoc: XmlDocument; XPath: Text): Text + var + XmlNode: XmlNode; + begin + if XmlDoc.SelectSingleNode(XPath, XmlNode) then + exit(XmlNode.AsXmlElement().InnerText()); + end; + + var + InvalidXmlErr: Label 'The French invoice lifecycle message is not valid XML.'; + InvoiceIDErr: Label 'The French invoice lifecycle message does not contain an invoice ID.'; + StatusErr: Label 'The French invoice lifecycle message does not contain a status.'; + UnsupportedStatusErr: Label 'French invoice lifecycle status %1 is not supported.', Comment = '%1 = lifecycle status'; + InvoiceMismatchErr: Label 'The lifecycle message invoice ID %1 does not match E-Document invoice %2.', Comment = '%1 = message invoice identifier, %2 = E-Document invoice identifier'; + RejectedReasonCodeErr: Label 'A technical rejection reason code is required.'; + RejectedReasonDescriptionErr: Label 'A technical rejection reason description is required.'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index 834166558a0..a7def2a5563 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -24,34 +24,50 @@ codeunit 10976 "FR E-Invoice Message Builder" AmountElement: XmlElement; OutStream: OutStream; begin + FREInvoiceMessage.TestField("Event Date"); XmlDoc := XmlDocument.Create(); XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('udt', UdtNamespaceTok)); RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok, XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); + AddIssueDateTime(AcknowledgementElement, FREInvoiceMessage."Event Date"); ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); - if FREInvoiceMessage.Type = FREInvoiceMessage.Type::Refused then begin - ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); - ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); - StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); - ReferenceElement.Add(StatusElement); - end else begin - ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); - ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); - AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); - AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); - StatusElement.Add(AmountElement); - ReferenceElement.Add(StatusElement); + case FREInvoiceMessage.Type of + FREInvoiceMessage.Type::Accepted: + begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, AcceptedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, AcceptedStatusNameTok)); + end; + FREInvoiceMessage.Type::Refused: + begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); + StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); + ReferenceElement.Add(StatusElement); + end; + FREInvoiceMessage.Type::Collected, + FREInvoiceMessage.Type::"Negative Collected": + begin + ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); + AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); + StatusElement.Add(AmountElement); + ReferenceElement.Add(StatusElement); + end; + else + Error(UnsupportedMessageTypeErr, FREInvoiceMessage.Type); end; AcknowledgementElement.Add(ReferenceElement); RootElement.Add(AcknowledgementElement); @@ -61,6 +77,18 @@ codeunit 10976 "FR E-Invoice Message Builder" XmlDoc.WriteTo(OutStream); end; + local procedure AddIssueDateTime(var AcknowledgementElement: XmlElement; EventDate: Date) + var + DateTimeStringElement: XmlElement; + IssueDateTimeElement: XmlElement; + begin + IssueDateTimeElement := XmlElement.Create('IssueDateTime', RamNamespaceTok); + DateTimeStringElement := XmlElement.Create('DateTimeString', UdtNamespaceTok, Format(EventDate, 0, '000000')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateTimeFormatCodeTok)); + IssueDateTimeElement.Add(DateTimeStringElement); + AcknowledgementElement.Add(IssueDateTimeElement); + end; + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] var GeneralLedgerSetup: Record "General Ledger Setup"; @@ -75,10 +103,15 @@ codeunit 10976 "FR E-Invoice Message Builder" var RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + UdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', Locked = true; + DateTimeFormatCodeTok: Label '204', Locked = true; InvoiceReferenceStatusCodeTok: Label '47', Locked = true; CollectedStatusCodeTok: Label '212', Locked = true; CollectedStatusNameTok: Label 'Encaissée', Locked = true; CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; RefusedStatusCodeTok: Label '210', Locked = true; RefusedStatusNameTok: Label 'Refusée', Locked = true; + AcceptedStatusCodeTok: Label '205', Locked = true; + AcceptedStatusNameTok: Label 'Acceptée', Locked = true; + UnsupportedMessageTypeErr: Label 'French invoice lifecycle message type %1 cannot be sent.', Comment = '%1 = French invoice lifecycle message type'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 6f2045d1eaa..96e662b785b 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -15,24 +15,21 @@ codeunit 10975 "FR E-Invoice Message Mgt." InherentEntitlements = X; InherentPermissions = X; + internal procedure AcceptInvoice(EDocument: Record "E-Document") + begin + CheckBuyerResponseAllowed(EDocument); + CreateAndSendMessage(EDocument, "FR E-Invoice Message Type"::Accepted, CreateGuid(), 0, '', Today(), 0, 0, '', ''); + end; + internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500]) - var - FREInvoiceMessage: Record "FR E-Invoice Message"; begin - EDocument.TestField(Direction, EDocument.Direction::Incoming); - EDocument.TestField("Document Type", EDocument."Document Type"::"Purchase Invoice"); - EDocument.TestField(Service); + CheckBuyerResponseAllowed(EDocument); if ReasonCode = '' then Error(ReasonCodeRequiredErr); if ReasonDescription = '' then Error(ReasonDescriptionRequiredErr); - FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); - FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Refused); - if not FREInvoiceMessage.IsEmpty() then - Error(AlreadyRefusedErr, EDocument."Document No."); - - CreateAndSendMessage(EDocument, FREInvoiceMessage.Type::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription); + CreateAndSendMessage(EDocument, "FR E-Invoice Message Type"::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription); end; internal procedure ProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") @@ -130,15 +127,34 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]); end; + local procedure CheckBuyerResponseAllowed(EDocument: Record "E-Document") + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + begin + EDocument.TestField(Direction, EDocument.Direction::Incoming); + EDocument.TestField("Document Type", EDocument."Document Type"::"Purchase Invoice"); + EDocument.TestField(Service); + + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetFilter(Type, '%1|%2', FREInvoiceMessage.Type::Accepted, FREInvoiceMessage.Type::Refused); + if not FREInvoiceMessage.IsEmpty() then + Error(AlreadyRespondedErr, EDocument."Document No."); + end; + local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type" begin - if MessageType = MessageType::Refused then - exit("E-Doc. Response Type"::Refused); - exit("E-Doc. Response Type"::None); + case MessageType of + MessageType::Accepted: + exit("E-Doc. Response Type"::Accepted); + MessageType::Refused: + exit("E-Doc. Response Type"::Refused); + else + exit("E-Doc. Response Type"::None); + end; end; var ReasonCodeRequiredErr: Label 'A refusal reason code is required.'; ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.'; - AlreadyRefusedErr: Label 'Invoice %1 has already been refused.', Comment = '%1 = invoice number'; + AlreadyRespondedErr: Label 'Invoice %1 already has a buyer response.', Comment = '%1 = invoice number'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index 3816ff3584e..697053d4b3c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -68,6 +68,16 @@ page 10973 "FR E-Invoice Messages" ApplicationArea = Basic, Suite; ToolTip = 'Specifies the related generic E-Document message entry.'; } + field("External Message ID"; Rec."External Message ID") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the identifier assigned to an incoming lifecycle message by the external service.'; + } + field("Received At"; Rec."Received At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the incoming lifecycle message was received.'; + } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al index f89c14b4c33..4e8263157c2 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al @@ -56,6 +56,22 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" CurrPage.Update(false); end; } + action(AcceptFREInvoice) + { + ApplicationArea = Basic, Suite; + Caption = 'Accept E-Invoice'; + Image = Approve; + ToolTip = 'Accept the incoming French electronic purchase invoice and send the response to the supplier.'; + Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice"); + + trigger OnAction() + var + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + FREInvoiceMessageMgt.AcceptInvoice(Rec); + CurrPage.Update(false); + end; + } } } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al index cae8ea1e412..8585e6e06b4 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al @@ -8,6 +8,10 @@ using Microsoft.eServices.EDocument.Processing.Message; enumextension 10974 "FR E-Doc. Response Type" extends "E-Doc. Response Type" { + value(10970; Submitted) + { + Caption = 'Submitted'; + } value(10971; Refused) { Caption = 'Refused'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al index af1abb67b3a..4c4842b88ed 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al @@ -2,6 +2,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. // ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using System.Utilities; + codeunit 148149 "E-Doc. FR Struct. Import Tests" { Subtype = Test; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al similarity index 100% rename from src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMessageSenderMock.Codeunit.al rename to src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 6b40e96c8b4..28282ed8a94 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -7,12 +7,11 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; -using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; -codeunit 148152 "FR E-Invoice Message Tests" +codeunit 148151 "FR E-Invoice Message Tests" { Subtype = Test; TestType = IntegrationTest; @@ -58,6 +57,7 @@ codeunit 148152 "FR E-Invoice Message Tests" Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('212'), 'The payload must contain status 212.'); Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('100'), 'The payload must contain the collected amount.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains(''), 'The payload must contain the AFNOR lifecycle event date.'); end; [Test] @@ -232,6 +232,352 @@ codeunit 148152 "FR E-Invoice Message Tests" Assert.ExpectedError('is not registered'); end; + [Test] + procedure BuyerAcceptanceQueuesAndSendsStatus205() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + + FREInvoiceMessageMgt.AcceptInvoice(EDocument); + + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Accepted); + Assert.RecordCount(FREInvoiceMessage, 1); + Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Acceptance must queue the message without invoking the connector.'); + SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Accepted); + Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Accepted message must be sent.'); + Assert.AreEqual("E-Doc. Response Type"::Accepted, MessageSenderMock.GetLastResponseType(), 'The child message must be Accepted.'); + Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('205'), 'The payload must contain status 205.'); + end; + + [Test] + procedure BuyerResponseCannotBeRepeated() + var + EDocument: Record "E-Document"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + + FREInvoiceMessageMgt.AcceptInvoice(EDocument); + asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Changed my mind.'); + Assert.ExpectedError('already has a buyer response'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure BuyerResponseCannotBeRepeatedAfterRefusal() + var + EDocument: Record "E-Document"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + + FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); + asserterror FREInvoiceMessageMgt.AcceptInvoice(EDocument); + Assert.ExpectedError('already has a buyer response'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure ReceiveSubmittedPersistsNormalizedMessage() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + ExternalMsgID: Text[250]; + ReceivedAt: DateTime; + FREntryNo: Integer; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); + ReceivedAt := CreateDateTime(20260101D, 120000T); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); + + FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, ReceivedAt, TempBlob); + + FREInvoiceMessage.Get(FREntryNo); + Assert.AreEqual(FREInvoiceMessage.Type::Submitted, FREInvoiceMessage.Type, 'FR type must be Submitted.'); + Assert.AreEqual(ExternalMsgID, FREInvoiceMessage."External Message ID", 'External message ID must be stored.'); + Assert.AreEqual(ReceivedAt, FREInvoiceMessage."Received At", 'Received timestamp must be persisted.'); + Assert.AreEqual("E-Document Direction"::Incoming, EDocumentMessageAPI.GetMessageDirection(FREInvoiceMessage."E-Document Message Entry No."), 'Generic message must be Incoming.'); + Assert.AreEqual("E-Doc. Message Status"::Received, EDocumentMessageAPI.GetMessageStatus(FREInvoiceMessage."E-Document Message Entry No."), 'Generic message status must be Received.'); + Assert.AreEqual("E-Doc. Response Type"::Submitted, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'Generic response must be Submitted.'); + end; + + [Test] + procedure ReceiveAcceptedPersistsNormalizedMessage() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + ExternalMsgID: Text[250]; + FREntryNo: Integer; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", '205', '', '')); + + FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + + FREInvoiceMessage.Get(FREntryNo); + Assert.AreEqual(FREInvoiceMessage.Type::Accepted, FREInvoiceMessage.Type, 'FR type must be Accepted.'); + Assert.AreEqual("E-Doc. Response Type"::Accepted, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'Generic response must be Accepted.'); + end; + + [Test] + procedure ReceiveTechnicalRejectedPersistsReason() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + ExternalMsgID: Text[250]; + FREntryNo: Integer; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejetée', 'SCHEMA', 'Schema validation failed')); + + FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + + FREInvoiceMessage.Get(FREntryNo); + Assert.AreEqual(FREInvoiceMessage.Type::"Technical Rejected", FREInvoiceMessage.Type, 'FR type must be Technical Rejected.'); + Assert.AreEqual('SCHEMA', Format(FREInvoiceMessage."Reason Code"), 'Reason code must be persisted.'); + Assert.AreEqual('Schema validation failed', FREInvoiceMessage."Reason Description", 'Reason description must be persisted.'); + Assert.AreEqual("E-Doc. Response Type"::Rejected, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'Generic response must be Rejected.'); + end; + + [Test] + procedure ReceiveMessageIsIdempotent() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + ExternalMsgID: Text[250]; + FirstEntryNo: Integer; + SecondEntryNo: Integer; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); + + FirstEntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); + SecondEntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + + Assert.AreEqual(FirstEntryNo, SecondEntryNo, 'Same external message ID must return same FR entry.'); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Submitted); + Assert.RecordCount(FREInvoiceMessage, 1); + end; + + [Test] + procedure ReceiveMessageRejectsInvalidXml() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText('not xml at all'); + + asserterror FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + + Assert.ExpectedError('not valid XML'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure ReceiveMessageRejectsUnsupportedStatus() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Unknown', '', '')); + + asserterror FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + + Assert.ExpectedError('is not supported'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure ReceiveMessageRejectsInvoiceMismatch() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml('WRONG-INVOICE-ID', 'Submitted', '', '')); + + asserterror FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + + Assert.ExpectedError('does not match'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure ReceiveTechnicalRejectedRequiresReasonCode() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', '', 'Something went wrong')); + + asserterror FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + + Assert.ExpectedError('reason code is required'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure ReceiveTechnicalRejectedRequiresReasonDescription() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + ExternalDocID: Text[250]; + begin + Initialize(); + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', 'SCHEMA', '')); + + asserterror FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + + Assert.ExpectedError('reason description is required'); + Assert.ExpectedErrorCode('Dialog'); + end; + + [Test] + procedure BuilderRejectsIncomingOnlyStatus() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageBuilder: Codeunit "FR E-Invoice Message Builder"; + TempBlob: Codeunit "Temp Blob"; + begin + Initialize(); + CreateIncomingEDocument(EDocument); + FREInvoiceMessage.Init(); + FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; + FREInvoiceMessage.Type := FREInvoiceMessage.Type::Submitted; + FREInvoiceMessage."Source Occurrence ID" := CreateGuid(); + FREInvoiceMessage."Event Date" := Today(); + FREInvoiceMessage."Created At" := CurrentDateTime(); + FREInvoiceMessage.Insert(); + + asserterror FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); + + Assert.ExpectedError('cannot be sent'); + Assert.ExpectedErrorCode('Dialog'); + end; + + local procedure BuildLifecycleXml(InvoiceID: Text; Status: Text; ReasonCode: Text; ReasonDescription: Text): Text + var + XmlText: TextBuilder; + begin + XmlText.Append(''); + XmlText.Append(''); + XmlText.Append(InvoiceID); + XmlText.Append(''); + XmlText.Append(''); + XmlText.Append(Status); + XmlText.Append(''); + if ReasonCode <> '' then begin + XmlText.Append(''); + XmlText.Append(ReasonCode); + XmlText.Append(''); + end; + if ReasonDescription <> '' then begin + XmlText.Append(''); + XmlText.Append(ReasonDescription); + XmlText.Append(''); + end; + XmlText.Append(''); + exit(XmlText.ToText()); + end; + local procedure SendFirstMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type") var FREInvoiceMessage: Record "FR E-Invoice Message"; @@ -283,6 +629,16 @@ codeunit 148152 "FR E-Invoice Message Tests" EDocument.Insert(); end; + local procedure CreateOutgoingEDocument(var EDocument: Record "E-Document") + begin + EDocument.Init(); + EDocument."Document No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(EDocument."Document No.")); + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + end; + local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") var InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index 7b4d69f4a5b..e0af9ca3c46 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -46,9 +46,9 @@ codeunit 148148 "Factur-X CII XML Tests" LibraryUtility: Codeunit "Library - Utility"; Assert: Codeunit Assert; CIIXMLBuilder: Codeunit "CII XML Builder"; - EDocHelpers: Codeunit "EDoc. Helpers"; FacturXFormat: Codeunit "Factur-X Format"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; + BuyerElectronicAddressRequiredErr: Label 'Electronic Address must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; DialogErrorCodeTok: Label 'Dialog', Locked = true; IsInitialized: Boolean; @@ -1388,52 +1388,44 @@ codeunit 148148 "Factur-X CII XML Tests" procedure FacturXBillingModeB1ForItemOnlyInvoice() var SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesInvoiceLine: Record "Sales Invoice Line"; - PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; - SourceDocumentLines: RecordRef; - OriginalView: Text; + TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] GetFrenchBillingMode returns B1 for an invoice with only Item lines + // [SCENARIO] Factur-X CII XML uses billing mode B1 for an invoice with only Item lines Initialize(); // [GIVEN] Posted sales invoice containing only an Item line SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithBillingModeLines(false)); - SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SourceDocumentLines.GetTable(SalesInvoiceLine); - OriginalView := SourceDocumentLines.GetView(false); - // [WHEN] GetFrenchBillingMode is called - // [THEN] Result = 'B1' and the source lines view is unchanged - Assert.AreEqual('B1', PeppolBIS30FRFormat.GetFrenchBillingMode(SourceDocumentLines), + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Billing mode = 'B1' + Assert.AreEqual('B1', + GetCIINodeValue(TempBlob, '//ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID'), StrSubstNo(IncorrectValueErr, 'BillingMode B1')); - Assert.AreEqual(OriginalView, SourceDocumentLines.GetView(false), StrSubstNo(IncorrectValueErr, 'Source Document Lines View')); end; [Test] procedure FacturXBillingModeM1ForMixedItemAndNonItemInvoice() var SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesInvoiceLine: Record "Sales Invoice Line"; - PeppolBIS30FRFormat: Codeunit "Peppol BIS 3.0 FR Format"; - SourceDocumentLines: RecordRef; - OriginalView: Text; + TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] GetFrenchBillingMode returns M1 for an invoice with both Item and G/L Account lines + // [SCENARIO] Factur-X CII XML uses billing mode M1 for an invoice with both Item and G/L Account lines Initialize(); // [GIVEN] Posted sales invoice containing Item and G/L Account lines SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceWithBillingModeLines(true)); - SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); - SourceDocumentLines.GetTable(SalesInvoiceLine); - OriginalView := SourceDocumentLines.GetView(false); - // [WHEN] GetFrenchBillingMode is called - // [THEN] Result = 'M1' and the source lines view is unchanged - Assert.AreEqual('M1', PeppolBIS30FRFormat.GetFrenchBillingMode(SourceDocumentLines), + // [WHEN] Create CII XML + CreateSalesInvoiceCIIXMLFromHeader(SalesInvoiceHeader, TempBlob); + + // [THEN] Billing mode = 'M1' + Assert.AreEqual('M1', + GetCIINodeValue(TempBlob, '//ram:BusinessProcessSpecifiedDocumentContextParameter/ram:ID'), StrSubstNo(IncorrectValueErr, 'BillingMode M1')); - Assert.AreEqual(OriginalView, SourceDocumentLines.GetView(false), StrSubstNo(IncorrectValueErr, 'Source Document Lines View')); end; #endregion @@ -1458,11 +1450,11 @@ codeunit 148148 "Factur-X CII XML Tests" asserterror CheckFacturX(SourceDocumentHeader); // [THEN] Error about buyer electronic address is raised - AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressRequiredError(CustomerNo)); + AssertExpectedDialogError(StrSubstNo(BuyerElectronicAddressRequiredErr, CustomerNo)); end; [Test] - procedure FacturXCheckRaisesErrorWhenBuyerElectronicAddressIsMalformed() + procedure FacturXCheckAcceptsNonemptyBuyerElectronicAddress() var SalesInvoiceHeader: Record "Sales Invoice Header"; Customer: Record Customer; @@ -1470,7 +1462,7 @@ codeunit 148148 "Factur-X CII XML Tests" CustomerNo: Code[20]; begin // [FEATURE] [AI test] - // [SCENARIO] Factur-X Format Check raises error when buyer electronic address does not match SIREN format + // [SCENARIO] Factur-X Format Check accepts a nonempty buyer electronic address Initialize(); // [GIVEN] Customer "C" with malformed FR Electronic Address (non-digit prefix) @@ -1485,11 +1477,8 @@ codeunit 148148 "Factur-X CII XML Tests" SourceDocumentHeader.GetTable(SalesInvoiceHeader); // [WHEN] Factur-X Format Check is called - asserterror CheckFacturX(SourceDocumentHeader); - - // [THEN] Error about malformed buyer identifier is raised - AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressInvalidError( - Customer.FieldCaption("FR Electronic Address"), CustomerNo)); + // [THEN] No error is raised + CheckFacturX(SourceDocumentHeader); end; [Test] @@ -1562,7 +1551,7 @@ codeunit 148148 "Factur-X CII XML Tests" asserterror CheckFacturX(SourceDocumentHeader); // [THEN] Error about buyer electronic address is raised - AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressRequiredError(CustomerNo)); + AssertExpectedDialogError(StrSubstNo(BuyerElectronicAddressRequiredErr, CustomerNo)); end; #endregion @@ -2141,7 +2130,7 @@ codeunit 148148 "Factur-X CII XML Tests" LibrarySales.CreateCustomer(Customer); Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); Customer."FR Electronic Address" := ''; - Customer."FR Elec. Address Scheme" := Customer."FR Elec. Address Scheme"::" "; + Clear(Customer."FR Elec. Address Scheme"); Customer."VAT Registration No." := ''; Customer."Registration Number" := ''; Customer.Modify(true); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al index 094363b074a..4c9d0ad8ca0 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al @@ -714,11 +714,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CountryRegion.Init(); CountryRegion.Code := CountryCode; CountryRegion.Name := CountryCode; - CountryRegion."ISO Code" := CountryCode; + CountryRegion."ISO Code" := CopyStr(CountryCode, 1, MaxStrLen(CountryRegion."ISO Code")); CountryRegion.Insert(true); end else if CountryRegion."ISO Code" = '' then begin - CountryRegion."ISO Code" := CountryCode; + CountryRegion."ISO Code" := CopyStr(CountryCode, 1, MaxStrLen(CountryRegion."ISO Code")); CountryRegion.Modify(true); end; end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index dac5d2a0222..c3f9a49b926 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -80,6 +80,38 @@ codeunit 6433 "E-Doc. Message Mgt." TempBlob := EDocDataStorage.GetTempBlob(); end; + procedure GetMessageEDocument(MessageEntryNo: Integer; var EDocument: Record "E-Document") + var + EDocMessage: Record "E-Document Message"; + begin + EDocMessage.Get(MessageEntryNo); + EDocument.Get(EDocMessage."E-Document Entry No."); + end; + + procedure GetMessageDirection(MessageEntryNo: Integer): Enum "E-Document Direction" + var + EDocMessage: Record "E-Document Message"; + begin + EDocMessage.Get(MessageEntryNo); + exit(EDocMessage.Direction); + end; + + procedure GetMessageStatus(MessageEntryNo: Integer): Enum "E-Doc. Message Status" + var + EDocMessage: Record "E-Document Message"; + begin + EDocMessage.Get(MessageEntryNo); + exit(EDocMessage.Status); + end; + + procedure GetMessageResponseType(MessageEntryNo: Integer): Enum "E-Doc. Response Type" + var + EDocMessage: Record "E-Document Message"; + begin + EDocMessage.Get(MessageEntryNo); + exit(EDocMessage."Response Type"); + end; + procedure SendMessage(MessageEntryNo: Integer) var EDocument: Record "E-Document"; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al similarity index 100% rename from src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageTransportDefault.Codeunit.al rename to src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al index 0800eda61c8..0400caaceb4 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceType.Enum.al @@ -7,7 +7,7 @@ namespace Microsoft.eServices.EDocument.Processing.Message; /// /// Identifies whether an E-Document payment occurrence applies or reverses an amount. /// -enum 6430 "E-Doc. Payment Occurrence Type" +enum 6115 "E-Doc. Payment Occurrence Type" { Access = Public; Extensible = false; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al index 06b7174d684..ad0cbc85a61 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -27,6 +27,54 @@ codeunit 6532 "E-Document Message API" exit(EDocMessageMgt.CreateMessage(EDocument, MessageType, EDocument.Direction::Outgoing, ResponseType, TempBlob)); end; + /// + /// Gets the parent E-Document of an E-Document message. + /// + /// The entry number of the E-Document message. + /// The parent E-Document. + procedure GetMessageEDocument(MessageEntryNo: Integer; var EDocument: Record "E-Document") + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.GetMessageEDocument(MessageEntryNo, EDocument); + end; + + /// + /// Gets the direction of an E-Document message. + /// + /// The entry number of the E-Document message. + /// The message direction. + procedure GetMessageDirection(MessageEntryNo: Integer): Enum "E-Document Direction" + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.GetMessageDirection(MessageEntryNo)); + end; + + /// + /// Gets the processing status of an E-Document message. + /// + /// The entry number of the E-Document message. + /// The message processing status. + procedure GetMessageStatus(MessageEntryNo: Integer): Enum "E-Doc. Message Status" + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.GetMessageStatus(MessageEntryNo)); + end; + + /// + /// Gets the response type represented by an E-Document message. + /// + /// The entry number of the E-Document message. + /// The message response type. + procedure GetMessageResponseType(MessageEntryNo: Integer): Enum "E-Doc. Response Type" + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + exit(EDocMessageMgt.GetMessageResponseType(MessageEntryNo)); + end; + /// /// Sends a previously created outgoing E-Document message through its E-Document service. /// diff --git a/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceExport.Codeunit.al b/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceExport.Codeunit.al index 40ffcfc6fb1..0e6cfac2fbf 100644 --- a/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceExport.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceExport.Codeunit.al @@ -75,8 +75,8 @@ codeunit 6531 "E-Doc. Remit. Advice Export" var DocumentSendingProfile: Record "Document Sending Profile"; EDocumentProcessing: Codeunit "E-Document Processing"; - AlreadyExists: Boolean; RecRef: RecordRef; + AlreadyExists: Boolean; begin RecRef.GetTable(PaymentVendLedgEntry); DocumentSendingProfile := EDocumentProcessing.GetDocSendingProfileForDocRef(RecRef); diff --git a/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceJournal.ReportExt.al b/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceJournal.ReportExt.al index b097f92c3c2..97d60e56888 100644 --- a/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceJournal.ReportExt.al +++ b/src/Apps/W1/EDocument/App/src/RemittanceAdvice/EDocRemitAdviceJournal.ReportExt.al @@ -45,13 +45,17 @@ reportextension 6100 "E-Doc. Remit. Advice Journal" extends "Remittance Advice - var LastAccountNo: Code[20]; LastDocumentNo: Code[20]; - LastJournalTemplateName: Code[10]; LastJournalBatchName: Code[10]; + LastJournalTemplateName: Code[10]; FirstGroup: Boolean; begin if not CreateEDocuments then exit; + Clear(LastAccountNo); + Clear(LastDocumentNo); + Clear(LastJournalBatchName); + Clear(LastJournalTemplateName); TempFlaggedGenJnlLine.Reset(); TempFlaggedGenJnlLine.SetCurrentKey("Journal Template Name", "Journal Batch Name", "Account No.", "Document No.", "Line No."); FirstGroup := true; @@ -78,9 +82,9 @@ reportextension 6100 "E-Doc. Remit. Advice Journal" extends "Remittance Advice - end; var + TempFlaggedGenJnlLine: Record "Gen. Journal Line" temporary; EDocRemittanceAdviceMgt: Codeunit "E-Doc. Remittance Advice Mgt."; EDocRemitAdviceExport: Codeunit "E-Doc. Remit. Advice Export"; - TempFlaggedGenJnlLine: Record "Gen. Journal Line" temporary; CreateEDocuments: Boolean; ReExportConfirmQst: Label 'An e-document was already created for this payment. Create again?'; From 53b2023de46dca61e650f30aad4df8b35497df20 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 17:21:26 +0200 Subject: [PATCH 05/49] tests updated --- .../test/src/FREDocMsgSenderMock.Codeunit.al | 7 +- .../src/FREInvoiceMessageTests.Codeunit.al | 64 ++++++++++++++++--- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al index bf0e20961d2..2144f875407 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al @@ -23,13 +23,18 @@ codeunit 148150 "FR E-Doc. Msg. Sender Mock" implements IDocumentSender, IDocume procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context") var TempBlob: Codeunit "Temp Blob"; + PayloadLine: Text; InStream: InStream; begin SendCount += 1; LastResponseType := MessageContext.GetResponseType(); TempBlob := MessageContext.GetTempBlob(); TempBlob.CreateInStream(InStream, TextEncoding::UTF8); - InStream.ReadText(LastPayload); + Clear(LastPayload); + while not InStream.EOS do begin + InStream.ReadText(PayloadLine); + LastPayload += PayloadLine; + end; if ReportSuccess then MessageContext.Status().SetStatus("E-Document Service Status"::Sent); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 28282ed8a94..9bd0f204d9a 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -55,9 +55,9 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage.FindFirst(); SendMessage(FREInvoiceMessage); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('212'), 'The payload must contain status 212.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('100'), 'The payload must contain the collected amount.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains(''), 'The payload must contain the AFNOR lifecycle event date.'); + AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '212'); + AssertPayloadAmount(MessageSenderMock.GetLastPayload(), 100, 'EUR'); + AssertPayloadDateFormat(MessageSenderMock.GetLastPayload(), '204'); end; [Test] @@ -99,7 +99,7 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'Unapplication must queue the reversal without invoking the connector.'); SendMessage(NegativeMessage); Assert.AreEqual(2, MessageSenderMock.GetSendCount(), 'Collected and Negative Collected messages must be sent.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('-100'), 'The reversal payload must contain a negative amount.'); + AssertPayloadAmount(MessageSenderMock.GetLastPayload(), -100, 'EUR'); end; [Test] @@ -117,8 +117,8 @@ codeunit 148151 "FR E-Invoice Message Tests" SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One refusal message must be sent.'); Assert.AreEqual("E-Doc. Response Type"::Refused, MessageSenderMock.GetLastResponseType(), 'The child message must be Refused.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('210'), 'The payload must contain status 210.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('PRICE'), 'The payload must contain the reason code.'); + AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '210'); + AssertPayloadReasonCode(MessageSenderMock.GetLastPayload(), 'PRICE'); end; [Test] @@ -132,10 +132,13 @@ codeunit 148151 "FR E-Invoice Message Tests" asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', 'Not accepted.'); Assert.ExpectedError('A refusal reason code is required.'); + Clear(EDocument); + CreateIncomingEDocument(EDocument); FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Again.'); - Assert.ExpectedError('has already been refused'); + Assert.ExpectedError('already has a buyer response'); + Assert.ExpectedErrorCode('Dialog'); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'A duplicate refusal must not be sent.'); end; @@ -251,7 +254,7 @@ codeunit 148151 "FR E-Invoice Message Tests" SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Accepted); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Accepted message must be sent.'); Assert.AreEqual("E-Doc. Response Type"::Accepted, MessageSenderMock.GetLastResponseType(), 'The child message must be Accepted.'); - Assert.IsTrue(MessageSenderMock.GetLastPayload().Contains('205'), 'The payload must contain status 205.'); + AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '205'); end; [Test] @@ -595,6 +598,51 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocumentMessageAPI.SendMessage(FREInvoiceMessage."E-Document Message Entry No."); end; + local procedure AssertPayloadAmount(Payload: Text; ExpectedAmount: Decimal; ExpectedCurrencyCode: Code[10]) + var + XmlDoc: XmlDocument; + AmountNode: XmlNode; + CurrencyCodeNode: XmlNode; + ActualAmount: Decimal; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain a value amount.'); + Assert.IsTrue(Evaluate(ActualAmount, AmountNode.AsXmlElement().InnerText(), 9), 'The payload value amount must be a valid XML decimal.'); + Assert.AreEqual(ExpectedAmount, ActualAmount, 'The payload value amount is incorrect.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]/@currencyID', CurrencyCodeNode), 'The payload value amount must contain a currency.'); + Assert.AreEqual(ExpectedCurrencyCode, CurrencyCodeNode.AsXmlAttribute().Value(), 'The payload currency is incorrect.'); + end; + + local procedure AssertPayloadStatus(Payload: Text; ExpectedStatus: Text) + var + XmlDoc: XmlDocument; + StatusNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ProcessConditionCode"]', StatusNode), 'The payload must contain a status.'); + Assert.AreEqual(ExpectedStatus, StatusNode.AsXmlElement().InnerText(), 'The payload status is incorrect.'); + end; + + local procedure AssertPayloadDateFormat(Payload: Text; ExpectedFormat: Text) + var + XmlDoc: XmlDocument; + DateFormatNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="DateTimeString"]/@format', DateFormatNode), 'The payload must contain an event date format.'); + Assert.AreEqual(ExpectedFormat, DateFormatNode.AsXmlAttribute().Value(), 'The payload event date format is incorrect.'); + end; + + local procedure AssertPayloadReasonCode(Payload: Text; ExpectedReasonCode: Text) + var + XmlDoc: XmlDocument; + ReasonCodeNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ReasonCode"]', ReasonCodeNode), 'The payload must contain a reason code.'); + Assert.AreEqual(ExpectedReasonCode, ReasonCodeNode.AsXmlElement().InnerText(), 'The payload reason code is incorrect.'); + end; + local procedure Initialize() var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; From bcef50bb247b749168b06f808d1e5bff4c7751a0 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 22:53:58 +0200 Subject: [PATCH 06/49] ai test updates --- .../src/FREInvoiceMessageTests.Codeunit.al | 72 +++++++++++++++++-- 1 file changed, 65 insertions(+), 7 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 9bd0f204d9a..152fea6d3c3 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -38,11 +38,17 @@ codeunit 148151 "FR E-Invoice Message Tests" DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment to an approved French E-Document creates a Collected message Initialize(); - CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + // [GIVEN] An approved outgoing French E-Document with an applied customer payment + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + + // [WHEN] The payment application is processed FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + // [THEN] One Collected lifecycle message and one generic payment occurrence are created and the message can be sent FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); Assert.RecordCount(FREInvoiceMessage, 1); @@ -60,6 +66,58 @@ codeunit 148151 "FR E-Invoice Message Tests" AssertPayloadDateFormat(MessageSenderMock.GetLastPayload(), '204'); end; + [Test] + procedure PaymentApplicationForClearedDocumentCreatesCollected() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment to a cleared French E-Document creates a Collected message + Initialize(); + + // [GIVEN] A cleared outgoing French E-Document with an applied customer payment + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Cleared); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] One Collected lifecycle message is created for the E-Document + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 1); + end; + + [Test] + procedure PaymentApplicationForSentDocumentDoesNotCreateCollected() + var + EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Applying a payment to a sent French E-Document does not create a Collected message + Initialize(); + + // [GIVEN] A sent outgoing French E-Document with an applied customer payment + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Sent); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The generic payment occurrence is created but no French lifecycle message is created + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 0); + end; + [Test] procedure PaymentUnapplicationSendsLinkedNegativeCollected() var @@ -73,7 +131,7 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin Initialize(); - CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); @@ -175,7 +233,7 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin Initialize(); - CreatePaymentScenario(EDocument, DetailedCustLedgEntry); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); @@ -687,7 +745,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument.Insert(); end; - local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status") var InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; @@ -706,7 +764,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; EDocument.Insert(); - CreateServiceStatus(EDocument); + CreateServiceStatus(EDocument, ServiceStatus); InvoiceCustLedgerEntry.Init(); InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); @@ -720,14 +778,14 @@ codeunit 148151 "FR E-Invoice Message Tests" CreateDetailedLedgerEntry(DetailedCustLedgEntry, InvoiceCustLedgerEntry."Entry No.", PaymentCustLedgerEntry."Entry No.", -100); end; - local procedure CreateServiceStatus(EDocument: Record "E-Document") + local procedure CreateServiceStatus(EDocument: Record "E-Document"; ServiceStatus: Enum "E-Document Service Status") var EDocumentServiceStatus: Record "E-Document Service Status"; begin EDocumentServiceStatus.Init(); EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No"; EDocumentServiceStatus."E-Document Service Code" := EDocument.Service; - EDocumentServiceStatus.Status := EDocumentServiceStatus.Status::Approved; + EDocumentServiceStatus.Status := ServiceStatus; EDocumentServiceStatus.Insert(); end; From 54fcc3326ba9cbb96772f9674c1be3763063e95c Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 23:14:15 +0200 Subject: [PATCH 07/49] PR comment changes --- .../EReportingFRUser.PermissionSet.al | 24 +++++ .../app/src/Core/FREInvoiceMessage.Table.al | 2 + .../src/FREInvoiceMessageTests.Codeunit.al | 32 ++++++- .../EDocCoreObjects.PermissionSet.al | 3 - .../Permissions/EDocCoreUser.PermissionSet.al | 4 + .../Interfaces/IMessageSender.Interface.al | 5 +- .../Message/EDocMessageMgt.Codeunit.al | 14 ++- .../Message/EDocMessageSendJob.Codeunit.al | 10 +- .../Message/EDocMessageSendRunner.Codeunit.al | 20 ++++ .../EDocMessageMgtTests.Codeunit.al | 93 +++++++++++++++++++ 10 files changed, 188 insertions(+), 19 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendRunner.Codeunit.al create mode 100644 src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al new file mode 100644 index 00000000000..a2ac9cd2a5e --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; + +permissionset 10988 "E-Reporting FR - User" +{ + Assignable = true; + Caption = 'E-Reporting FR - User'; + + IncludedPermissionSets = "E-Doc. Core - User"; + + Permissions = + table "FR E-Invoice Message" = X, + tabledata "FR E-Invoice Message" = RIMD, + codeunit "FR E-Invoice Message Mgt." = X, + codeunit "FR E-Invoice Message Builder" = X, + codeunit "FR E-Invoice Message API" = X, + page "FR E-Invoice Refusal Dialog" = X, + page "FR E-Invoice Messages" = X; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 64911991e0c..1a9d62e3a40 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; table 10970 "FR E-Invoice Message" { @@ -80,6 +81,7 @@ table 10970 "FR E-Invoice Message" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; + TableRelation = "E-Document Message"."Entry No."; } field(13; "Created At"; DateTime) { diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 152fea6d3c3..4b6be201cfb 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -190,6 +190,7 @@ codeunit 148151 "FR E-Invoice Message Tests" asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', 'Not accepted.'); Assert.ExpectedError('A refusal reason code is required.'); + Assert.ExpectedErrorCode('Dialog'); Clear(EDocument); CreateIncomingEDocument(EDocument); FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); @@ -200,6 +201,27 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'A duplicate refusal must not be sent.'); end; + [Test] + procedure RefusalRequiresReasonDescription() + var + EDocument: Record "E-Document"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A buyer refusal requires a reason description + Initialize(); + + // [GIVEN] An incoming French purchase invoice + CreateIncomingEDocument(EDocument); + + // [WHEN] The invoice is refused without a reason description + asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', ''); + + // [THEN] The refusal is rejected + Assert.ExpectedError('A refusal reason description is required.'); + Assert.ExpectedErrorCode('Dialog'); + end; + [Test] procedure MessageSenderMustReportSuccess() var @@ -253,20 +275,24 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocumentMessageAPI: Codeunit "E-Document Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; + ExternalDocumentID: Text[250]; + ExternalMessageID: Text[250]; FirstMessageEntryNo: Integer; DuplicateMessageEntryNo: Integer; begin Initialize(); CreateIncomingEDocument(EDocument); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, 'FR-DOC-001'); + ExternalDocumentID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalDocumentID)); + ExternalMessageID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalMessageID)); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocumentID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); FirstMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( - EDocument.Service, 'FR-DOC-001', 'FR-MSG-001', "E-Document Message Type"::"FR Invoice Lifecycle", + EDocument.Service, ExternalDocumentID, ExternalMessageID, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); DuplicateMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( - EDocument.Service, 'FR-DOC-001', 'FR-MSG-001', "E-Document Message Type"::"FR Invoice Lifecycle", + EDocument.Service, ExternalDocumentID, ExternalMessageID, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); Assert.AreNotEqual(0, FirstMessageEntryNo, 'The incoming lifecycle message must be persisted.'); diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index bf976ecd725..b5c4b2029d1 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -106,12 +106,9 @@ permissionset 6100 "E-Doc. Core - Objects" #endif codeunit "E-Doc. Attachment Processor" = X, codeunit "E-Doc. Hist. Line Data Loader" = X, - codeunit "E-Document Message API" = X, codeunit "E-Doc. Message Context" = X, codeunit "E-Doc. Message Mgt." = X, - codeunit "E-Doc. Message Send Job" = X, codeunit "E-Doc. Msg. Transport Default" = X, - codeunit "E-Doc. Payment Occurrence Mgt." = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, page "E-Doc. Changes Preview" = X, diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al index 6cc4d4dc41f..e8aeadb0579 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al @@ -25,6 +25,10 @@ permissionset 6105 "E-Doc. Core - User" IncludedPermissionSets = "E-Doc. Core - Read"; Permissions = + codeunit "E-Document Message API" = X, + codeunit "E-Doc. Message Send Job" = X, + codeunit "E-Doc. Message Send Runner" = X, + codeunit "E-Doc. Payment Occurrence Mgt." = X, tabledata "E-Document" = iMD, #region Service tabledata "E-Document Service" = im, diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al index 3f446ebd4a0..3204c010ba4 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al @@ -18,6 +18,9 @@ interface IMessageSender /// The parent E-Document. /// The service used to send the message. /// The message payload, transport state, and result. The implementation must set the result to Sent after successful transmission. - /// The implementation is responsible for obtaining any privacy consent required by the external service before transmitting data. + /// + /// The implementation is responsible for obtaining any privacy consent required by the external service before transmitting data. + /// Implementations must use MessageContext.GetMessageEntryNo() as an idempotency key because a message can be retried after an ambiguous transport failure. + /// procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"); } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index c3f9a49b926..a0960055650 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -121,6 +121,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; + MessageSendingErrorInfo: ErrorInfo; begin EDocMessage.Get(MessageEntryNo); EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); @@ -137,8 +138,12 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); - if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then - Error(MessageSendingErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then begin + MessageSendingErrorInfo.ErrorType := ErrorType::Internal; + MessageSendingErrorInfo.Message := StrSubstNo(MessageSendingErr, MessageEntryNo); + MessageSendingErrorInfo.DetailedMessage := StrSubstNo(MessageSendingDetailedErr, EDocMessageContext.Status().GetStatus()); + Error(MessageSendingErrorInfo); + end; EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); @@ -160,6 +165,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage.Status := EDocMessage.Status::Queued; EDocMessage.Modify(); + Commit(); EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); end; @@ -204,6 +210,7 @@ codeunit 6433 "E-Doc. Message Mgt." if not TempBlob.HasValue() then Error(IncomingMessagePayloadRequiredErr); + EDocMessage.LockTable(); EDocMessage.SetRange(Service, ServiceCode); EDocMessage.SetRange("External Message ID", ExternalMessageID); if EDocMessage.FindFirst() then @@ -248,7 +255,8 @@ codeunit 6433 "E-Doc. Message Mgt." var MessagePayloadErr: Label 'E-Document message %1 does not contain a payload.', Comment = '%1 = E-Document message entry number'; - MessageSendingErr: Label 'E-Document message %1 could not be sent. The integration returned status %2.', Comment = '%1 = E-Document message entry number, %2 = integration status'; + MessageSendingErr: Label 'E-Document message %1 could not be sent.', Comment = '%1 = E-Document message entry number'; + MessageSendingDetailedErr: Label 'The E-Document message integration returned status %1.', Comment = '%1 = integration status'; ExternalDocumentIDRequiredErr: Label 'An external document ID is required.'; ExternalMessageIDRequiredErr: Label 'An external message ID is required.'; IncomingMessagePayloadRequiredErr: Label 'An incoming E-Document message payload is required.'; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al index 8d43c26cb34..28f31c79d3d 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -19,7 +19,7 @@ codeunit 6535 "E-Doc. Message Send Job" LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); - if TrySendMessage(EDocumentMessage."Entry No.") then + if Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) then exit; LastErrorText := GetLastErrorText(); @@ -33,14 +33,6 @@ codeunit 6535 "E-Doc. Message Send Job" Error(MessageSendFailedErr, EDocumentMessage."Entry No.", LastErrorText); end; - [TryFunction] - local procedure TrySendMessage(MessageEntryNo: Integer) - var - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; - begin - EDocMessageMgt.SendMessage(MessageEntryNo); - end; - var MessageSendFailedErr: Label 'E-Document message %1 could not be sent. %2', Comment = '%1 = message entry number, %2 = connector error'; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendRunner.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendRunner.Codeunit.al new file mode 100644 index 00000000000..3733c43cf8b --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendRunner.Codeunit.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +codeunit 6537 "E-Doc. Message Send Runner" +{ + Access = Internal; + TableNo = "E-Document Message"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.SendMessage(Rec."Entry No."); + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al new file mode 100644 index 00000000000..3d1deb9532f --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -0,0 +1,93 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Sales.Customer; +using System.Threading; +using System.Utilities; + +codeunit 139899 "E-Doc. Message Mgt. Tests" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + EDocumentService: Record "E-Document Service"; + Assert: Codeunit Assert; + LibraryEDoc: Codeunit "Library - E-Document"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + IsInitialized: Boolean; + + [Test] + procedure QueueMessageSchedulesBackgroundSend() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] Queueing an outgoing E-Document message schedules its background send job + Initialize(Customer); + + // [GIVEN] A created outgoing E-Document message + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::"Unspecified", "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + + // [WHEN] The message is queued + EDocMessageMgt.QueueMessage(MessageEntryNo); + + // [THEN] The message is marked Queued and a send job is scheduled for it + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual("E-Doc. Message Status"::Queued, EDocMessage.Status, 'The message must be queued.'); + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Send Job"); + JobQueueEntry.SetRange("Record ID to Process", EDocMessage.RecordId()); + Assert.RecordCount(JobQueueEntry, 1); + end; + + local procedure Initialize(var Customer: Record Customer) + var + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + begin + LibraryLowerPermission.SetOutsideO365Scope(); + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Send Job"); + JobQueueEntry.DeleteAll(); + EDocMessage.DeleteAll(); + EDocument.DeleteAll(); + + if IsInitialized then + exit; + + LibraryEDoc.SetupStandardVAT(); + EDocumentService.DeleteAll(); + LibraryEDoc.SetupStandardSalesScenario( + Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock); + IsInitialized := true; + end; + + local procedure CreateOutgoingEDocument(var EDocument: Record "E-Document") + begin + EDocument.Init(); + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument.Service := EDocumentService.Code; + EDocument.Insert(); + end; +} From 6cff42535dbad7fa7caa6ffdec11b089713f03ef Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Thu, 20 Aug 2026 23:43:42 +0200 Subject: [PATCH 08/49] new comments fixes --- .../src/FREInvoiceMessageTests.Codeunit.al | 79 +++++++++++++------ .../EDocMsgTransportDefault.Codeunit.al | 9 ++- .../EDocPaymentOccurrenceMgt.Codeunit.al | 13 ++- 3 files changed, 70 insertions(+), 31 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 4b6be201cfb..35ef0201902 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -7,6 +7,11 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.Enums; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -27,6 +32,8 @@ codeunit 148151 "FR E-Invoice Message Tests" var Assert: Codeunit Assert; + LibraryERM: Codeunit "Library - ERM"; + LibrarySales: Codeunit "Library - Sales"; MessageSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock"; [Test] @@ -773,18 +780,37 @@ codeunit 148151 "FR E-Invoice Message Tests" local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status") var - InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; - PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + SalesHeader: Record "Sales Header"; SalesInvoiceHeader: Record "Sales Invoice Header"; - DocumentNo: Code[20]; - begin - DocumentNo := CopyStr(Format(CreateGuid()), 1, MaxStrLen(DocumentNo)); - SalesInvoiceHeader.Init(); - SalesInvoiceHeader."No." := DocumentNo; - SalesInvoiceHeader.Insert(); + SalesLine: Record "Sales Line"; + VATPostingSetup: Record "VAT Posting Setup"; + PostedInvoiceNo: Code[20]; + begin + EDocumentService.Get('FR-MESSAGE-MOCK'); + EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + EDocumentService.Modify(); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 0); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService.Modify(); + SalesInvoiceHeader.Get(PostedInvoiceNo); EDocument.Init(); - EDocument."Document No." := DocumentNo; + EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; @@ -792,16 +818,18 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument.Insert(); CreateServiceStatus(EDocument, ServiceStatus); - InvoiceCustLedgerEntry.Init(); - InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); - InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; - InvoiceCustLedgerEntry."Document No." := DocumentNo; - InvoiceCustLedgerEntry.Insert(); - PaymentCustLedgerEntry.Init(); - PaymentCustLedgerEntry."Entry No." := InvoiceCustLedgerEntry."Entry No." + 1; - PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; - PaymentCustLedgerEntry.Insert(); - CreateDetailedLedgerEntry(DetailedCustLedgEntry, InvoiceCustLedgerEntry."Entry No.", PaymentCustLedgerEntry."Entry No.", -100); + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, + GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -100); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + + FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); end; local procedure CreateServiceStatus(EDocument: Record "E-Document"; ServiceStatus: Enum "E-Document Service Status") @@ -829,13 +857,18 @@ codeunit 148151 "FR E-Invoice Message Tests" DetailedCustLedgEntry.Insert(); end; - local procedure GetNextCustLedgerEntryNo(): Integer + local procedure FindApplicationDetailedEntry(var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; InvoiceDocNo: Code[20]) var CustLedgerEntry: Record "Cust. Ledger Entry"; begin - if CustLedgerEntry.FindLast() then - exit(CustLedgerEntry."Entry No." + 1); - exit(1); + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + CustLedgerEntry.SetRange("Document No.", InvoiceDocNo); + CustLedgerEntry.FindFirst(); + DetailedCustLedgEntry.SetRange("Cust. Ledger Entry No.", CustLedgerEntry."Entry No."); + DetailedCustLedgEntry.SetRange("Entry Type", DetailedCustLedgEntry."Entry Type"::Application); + DetailedCustLedgEntry.SetRange("Initial Document Type", DetailedCustLedgEntry."Initial Document Type"::Invoice); + DetailedCustLedgEntry.SetFilter(Amount, '<%1', 0); + DetailedCustLedgEntry.FindLast(); end; local procedure GetNextDetailedLedgerEntryNo(): Integer diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al index 4b146cc1b46..1a407bc3c75 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al @@ -14,10 +14,17 @@ codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender InherentPermissions = X; procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context") + var + MessageTransportErrorInfo: ErrorInfo; begin - Error(MessageTransportNotSupportedErr, EDocumentService.Code); + MessageTransportErrorInfo.Message := StrSubstNo(MessageTransportNotSupportedErr, EDocumentService.Code); + MessageTransportErrorInfo.RecordId := EDocumentService.RecordId; + MessageTransportErrorInfo.PageNo := Page::"E-Document Service"; + MessageTransportErrorInfo.AddNavigationAction(ShowEDocumentServiceLbl); + Error(MessageTransportErrorInfo); end; var MessageTransportNotSupportedErr: Label 'E-Document service %1 does not support sending E-Document messages.', Comment = '%1 = E-Document service code'; + ShowEDocumentServiceLbl: Label 'Open E-Document Service'; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 2114fec8103..5be2d3c7ffc 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -63,7 +63,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." repeat CreateOccurrence( - EDocument, "E-Doc. Payment Occurrence Type"::Applied, DetailedCustLedgEntry.SystemId, + EDocument."Entry No", "E-Doc. Payment Occurrence Type"::Applied, DetailedCustLedgEntry.SystemId, -DetailedCustLedgEntry.Amount, DetailedCustLedgEntry."Currency Code", DetailedCustLedgEntry."Posting Date", DetailedCustLedgEntry."Entry No.", 0); until EDocument.Next() = 0; @@ -77,7 +77,6 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." procedure ProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") var AppliedOccurrence: Record "E-Doc. Payment Occurrence"; - EDocument: Record "E-Document"; begin if not IsInvoiceApplication(OldDetailedCustLedgEntry) then exit; @@ -88,26 +87,25 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." exit; repeat - EDocument.Get(AppliedOccurrence."E-Document Entry No."); CreateOccurrence( - EDocument, "E-Doc. Payment Occurrence Type"::Reversed, NewDetailedCustLedgEntry.SystemId, + AppliedOccurrence."E-Document Entry No.", "E-Doc. Payment Occurrence Type"::Reversed, NewDetailedCustLedgEntry.SystemId, -AppliedOccurrence.Amount, AppliedOccurrence."Currency Code", NewDetailedCustLedgEntry."Posting Date", NewDetailedCustLedgEntry."Entry No.", AppliedOccurrence."Entry No."); until AppliedOccurrence.Next() = 0; end; - local procedure CreateOccurrence(EDocument: Record "E-Document"; OccurrenceType: Enum "E-Doc. Payment Occurrence Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) + local procedure CreateOccurrence(EDocumentEntryNo: Integer; OccurrenceType: Enum "E-Doc. Payment Occurrence Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; begin - EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocumentEntryNo); EDocPaymentOccurrence.SetRange("Source Occurrence ID", SourceOccurrenceID); EDocPaymentOccurrence.SetRange(Type, OccurrenceType); if not EDocPaymentOccurrence.IsEmpty() then exit; EDocPaymentOccurrence.Init(); - EDocPaymentOccurrence."E-Document Entry No." := EDocument."Entry No"; + EDocPaymentOccurrence."E-Document Entry No." := EDocumentEntryNo; EDocPaymentOccurrence.Type := OccurrenceType; EDocPaymentOccurrence."Source Occurrence ID" := SourceOccurrenceID; EDocPaymentOccurrence."Original Occurrence Entry No." := OriginalOccurrenceEntryNo; @@ -129,6 +127,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then exit(false); + EDocument.SetLoadFields("Entry No"); EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); EDocument.SetRange(Direction, EDocument.Direction::Outgoing); EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); From 6a85318e9f98a53df198df32e075c1b9addd5d6d Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 10:53:26 +0200 Subject: [PATCH 09/49] new --- .../app/Permissions/EReportingFRUser.PermissionSet.al | 2 +- .../EReportingFR/app/src/Core/FREInvoiceMessage.Table.al | 2 -- .../app/src/Core/FREInvoiceMessageAPI.Codeunit.al | 2 +- .../app/src/Core/FREInvoiceMessageBuilder.Codeunit.al | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al index a2ac9cd2a5e..774bb0eea17 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al @@ -6,7 +6,7 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; -permissionset 10988 "E-Reporting FR - User" +permissionset 10988 "E-Reporting FR User" { Assignable = true; Caption = 'E-Reporting FR - User'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 1a9d62e3a40..64911991e0c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -5,7 +5,6 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; -using Microsoft.eServices.EDocument.Processing.Message; table 10970 "FR E-Invoice Message" { @@ -81,7 +80,6 @@ table 10970 "FR E-Invoice Message" { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; - TableRelation = "E-Document Message"."Entry No."; } field(13; "Created At"; DateTime) { diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al index 38aaa194bd6..7568dd60e15 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al @@ -101,7 +101,7 @@ codeunit 10987 "FR E-Invoice Message API" exit(MessageType::Submitted); '205', 'ACCEPTED', 'ACCEPTÉE', 'ACCEPTEE', 'APPROUVÉE', 'APPROUVEE': exit(MessageType::Accepted); - 'REJECTED', 'TECHNICAL REJECTED', 'REJETÉE', 'REJETEE': + '213', 'REJECTED', 'TECHNICAL REJECTED', 'REJETÉE', 'REJETEE': exit(MessageType::"Technical Rejected"); '210', 'REFUSED', 'REFUSÉE', 'REFUSEE': exit(MessageType::Refused); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index a7def2a5563..e1475aec66d 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -35,7 +35,7 @@ codeunit 10976 "FR E-Invoice Message Builder" XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); - AddIssueDateTime(AcknowledgementElement, FREInvoiceMessage."Event Date"); + AddIssueDateTime(AcknowledgementElement, FREInvoiceMessage."Event Date"); ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); @@ -112,6 +112,6 @@ codeunit 10976 "FR E-Invoice Message Builder" RefusedStatusCodeTok: Label '210', Locked = true; RefusedStatusNameTok: Label 'Refusée', Locked = true; AcceptedStatusCodeTok: Label '205', Locked = true; - AcceptedStatusNameTok: Label 'Acceptée', Locked = true; + AcceptedStatusNameTok: Label 'Approuvée', Locked = true; UnsupportedMessageTypeErr: Label 'French invoice lifecycle message type %1 cannot be sent.', Comment = '%1 = French invoice lifecycle message type'; } \ No newline at end of file From 19a729bd5e0e30bd3fea4113080bb31b7c3b8645 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 12:28:02 +0200 Subject: [PATCH 10/49] vat changes --- .../EReportingFRUser.PermissionSet.al | 2 + .../Core/FREInvoiceMessageBuilder.Codeunit.al | 43 +- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 236 ++++++++- .../src/Core/FREInvoiceMessageVAT.Table.al | 82 +++ .../src/FREInvoiceMessageTests.Codeunit.al | 479 +++++++++++++++++- 5 files changed, 828 insertions(+), 14 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al index 774bb0eea17..f1704d10295 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al @@ -16,6 +16,8 @@ permissionset 10988 "E-Reporting FR User" Permissions = table "FR E-Invoice Message" = X, tabledata "FR E-Invoice Message" = RIMD, + table "FR E-Invoice Message VAT" = X, + tabledata "FR E-Invoice Message VAT" = R, codeunit "FR E-Invoice Message Mgt." = X, codeunit "FR E-Invoice Message Builder" = X, codeunit "FR E-Invoice Message API" = X, diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index e1475aec66d..745f0583ebd 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -21,7 +21,6 @@ codeunit 10976 "FR E-Invoice Message Builder" AcknowledgementElement: XmlElement; ReferenceElement: XmlElement; StatusElement: XmlElement; - AmountElement: XmlElement; OutStream: OutStream; begin FREInvoiceMessage.TestField("Event Date"); @@ -59,12 +58,7 @@ codeunit 10976 "FR E-Invoice Message Builder" begin ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); - AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessage.Amount, 0, 9)); - AmountElement.Add(XmlAttribute.Create('currencyID', ResolveCurrencyCode(FREInvoiceMessage."Currency Code"))); - StatusElement.Add(AmountElement); - ReferenceElement.Add(StatusElement); + AddVATBreakdown(ReferenceElement, FREInvoiceMessage); end; else Error(UnsupportedMessageTypeErr, FREInvoiceMessage.Type); @@ -77,6 +71,40 @@ codeunit 10976 "FR E-Invoice Message Builder" XmlDoc.WriteTo(OutStream); end; + local procedure AddVATBreakdown(var ReferenceElement: XmlElement; FREInvoiceMessage: Record "FR E-Invoice Message") + var + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + StatusElement: XmlElement; + CurrencyCode: Code[10]; + begin + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + if not FREInvoiceMessageVAT.FindSet() then + Error(VATBreakdownErr, FREInvoiceMessage."Entry No."); + + CurrencyCode := ResolveCurrencyCode(FREInvoiceMessage."Currency Code"); + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + repeat + StatusElement.Add(CreateVATCharacteristic(FREInvoiceMessageVAT, CurrencyCode)); + until FREInvoiceMessageVAT.Next() = 0; + ReferenceElement.Add(StatusElement); + end; + + local procedure CreateVATCharacteristic(FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; CurrencyCode: Code[10]) CharacteristicElement: XmlElement + var + AmountElement: XmlElement; + ValueChangedElement: XmlElement; + begin + CharacteristicElement := XmlElement.Create('SpecifiedDocumentCharacteristic', RamNamespaceTok); + CharacteristicElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, CollectedAmountTypeCodeTok)); + ValueChangedElement := XmlElement.Create('ValueChangedIndicator', RamNamespaceTok); + ValueChangedElement.Add(XmlElement.Create('IndicatorString', UdtNamespaceTok, 'false')); + CharacteristicElement.Add(ValueChangedElement); + AmountElement := XmlElement.Create('ValueAmount', RamNamespaceTok, Format(FREInvoiceMessageVAT.Amount, 0, 9)); + AmountElement.Add(XmlAttribute.Create('currencyID', CurrencyCode)); + CharacteristicElement.Add(AmountElement); + CharacteristicElement.Add(XmlElement.Create('ValuePercent', RamNamespaceTok, Format(FREInvoiceMessageVAT."VAT %", 0, 9))); + end; + local procedure AddIssueDateTime(var AcknowledgementElement: XmlElement; EventDate: Date) var DateTimeStringElement: XmlElement; @@ -113,5 +141,6 @@ codeunit 10976 "FR E-Invoice Message Builder" RefusedStatusNameTok: Label 'Refusée', Locked = true; AcceptedStatusCodeTok: Label '205', Locked = true; AcceptedStatusNameTok: Label 'Approuvée', Locked = true; + VATBreakdownErr: Label 'French invoice message %1 does not have the VAT breakdown required for a collected status message.', Comment = '%1 = French invoice message entry number'; UnsupportedMessageTypeErr: Label 'French invoice lifecycle message type %1 cannot be sent.', Comment = '%1 = French invoice lifecycle message type'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 96e662b785b..97989b107f3 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -6,6 +6,10 @@ namespace Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.Currency; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.VAT.Ledger; +using Microsoft.Finance.VAT.Setup; using Microsoft.Sales.Receivables; using System.Utilities; @@ -15,6 +19,8 @@ codeunit 10975 "FR E-Invoice Message Mgt." InherentEntitlements = X; InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Message VAT" = ri; + internal procedure AcceptInvoice(EDocument: Record "E-Document") begin CheckBuyerResponseAllowed(EDocument); @@ -58,6 +64,9 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit; if EDocPaymentOccurrence.Type = EDocPaymentOccurrence.Type::Applied then begin + if not IsCollectedReportingRequired(EDocument, EDocPaymentOccurrence."Detailed Ledger Entry No.") then + exit; + CreateAndSendMessage( EDocument, "FR E-Invoice Message Type"::Collected, EDocPaymentOccurrence."Source Occurrence ID", EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date", @@ -75,7 +84,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." CreateAndSendMessage( EDocument, "FR E-Invoice Message Type"::"Negative Collected", EDocPaymentOccurrence."Source Occurrence ID", - EDocPaymentOccurrence.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date", + -CollectedMessage.Amount, EDocPaymentOccurrence."Currency Code", EDocPaymentOccurrence."Event Date", EDocPaymentOccurrence."Detailed Ledger Entry No.", CollectedMessage."Entry No.", '', ''); end; @@ -106,6 +115,13 @@ codeunit 10975 "FR E-Invoice Message Mgt." FREInvoiceMessage."Created At" := CurrentDateTime(); FREInvoiceMessage.Insert(); + case MessageType of + MessageType::Collected: + CreateCollectedVATBreakdown(EDocument, FREInvoiceMessage); + MessageType::"Negative Collected": + CreateReversalVATBreakdown(FREInvoiceMessage, OriginalEntryNo); + end; + FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); FREInvoiceMessage."E-Document Message Entry No." := EDocumentMessageAPI.CreateMessage( EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", GetResponseType(MessageType), TempBlob); @@ -113,6 +129,206 @@ codeunit 10975 "FR E-Invoice Message Mgt." EDocumentMessageAPI.QueueMessage(FREInvoiceMessage."E-Document Message Entry No."); end; + local procedure CreateCollectedVATBreakdown(EDocument: Record "E-Document"; var FREInvoiceMessage: Record "FR E-Invoice Message") + var + VATEntry: Record "VAT Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + AmountByVATKey: Dictionary of [Text, Decimal]; + VATCategoryByKey: Dictionary of [Text, Text]; + VATRateByKey: Dictionary of [Text, Decimal]; + VATKeys: List of [Text]; + CurrencyCode: Code[10]; + VATCategoryCode: Text; + VATKey: Text; + EligibleGrossAmount: Decimal; + GrossAmount: Decimal; + TotalGrossAmount: Decimal; + VATRate: Decimal; + begin + CurrencyCode := ResolveCurrencyCode(FREInvoiceMessage."Currency Code"); + FindInvoiceVATEntries(VATEntry, EDocument, FREInvoiceMessage."Detailed Ledger Entry No."); + VATEntry.SetLoadFields( + "VAT Bus. Posting Group", "VAT Prod. Posting Group", "Source Currency Code", + "Source Currency VAT Base", "Source Currency VAT Amount", Base, Amount, + "VAT Calculation Type", "Tax Jurisdiction Code", "Unrealized Amount", "Unrealized Base"); + if VATEntry.FindSet() then + repeat + GrossAmount := GetVATEntryGrossAmount(VATEntry, CurrencyCode); + TotalGrossAmount += GrossAmount; + if IsVATEntryReportable(VATEntry) then begin + VATPostingSetup.Get(VATEntry."VAT Bus. Posting Group", VATEntry."VAT Prod. Posting Group"); + VATRate := VATPostingSetup."VAT %"; + VATCategoryCode := VATPostingSetup."Tax Category"; + VATKey := GetVATAllocationKey(VATRate, VATCategoryCode); + AddVATAllocationBasis( + AmountByVATKey, VATRateByKey, VATCategoryByKey, VATKeys, + VATKey, VATRate, VATCategoryCode, GrossAmount); + EligibleGrossAmount += GrossAmount; + end; + until VATEntry.Next() = 0; + + if (VATKeys.Count() = 0) or (EligibleGrossAmount = 0) or (TotalGrossAmount = 0) then + Error(VATBreakdownErr, EDocument."Document No."); + + FREInvoiceMessage.Amount := Round( + FREInvoiceMessage.Amount * EligibleGrossAmount / TotalGrossAmount, + GetAmountRoundingPrecision(CurrencyCode)); + FREInvoiceMessage.Modify(); + InsertAllocatedVATAmounts( + FREInvoiceMessage, AmountByVATKey, VATRateByKey, VATCategoryByKey, + VATKeys, EligibleGrossAmount, CurrencyCode); + end; + + local procedure FindInvoiceVATEntries(var VATEntry: Record "VAT Entry"; EDocument: Record "E-Document"; DetailedLedgerEntryNo: Integer) + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + begin + DetailedCustLedgEntry.Get(DetailedLedgerEntryNo); + CustLedgerEntry.Get(DetailedCustLedgEntry."Cust. Ledger Entry No."); + VATEntry.SetRange(Type, VATEntry.Type::Sale); + VATEntry.SetRange("Document Type", VATEntry."Document Type"::Invoice); + VATEntry.SetRange("Document No.", EDocument."Document No."); + VATEntry.SetRange("Posting Date", EDocument."Posting Date"); + VATEntry.SetRange("Transaction No.", CustLedgerEntry."Transaction No."); + end; + + local procedure GetVATEntryGrossAmount(VATEntry: Record "VAT Entry"; CurrencyCode: Code[10]): Decimal + var + VATEntryCurrencyErrorInfo: ErrorInfo; + begin + if VATEntry."Source Currency Code" = CurrencyCode then + exit(-(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount")); + if VATEntry."Source Currency Code" = '' then + exit(-(VATEntry.Base + VATEntry.Amount)); + + VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); + VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); + Error(VATEntryCurrencyErrorInfo); + end; + + local procedure IsVATEntryReportable(VATEntry: Record "VAT Entry"): Boolean + begin + exit( + (VATEntry.GetUnrealizedVATType() > 0) and + ((VATEntry."Unrealized Amount" <> 0) or (VATEntry."Unrealized Base" <> 0))); + end; + + local procedure GetVATAllocationKey(VATRate: Decimal; VATCategoryCode: Text): Text + begin + exit(StrSubstNo('%1|%2', Format(VATRate, 0, 9), VATCategoryCode)); + end; + + local procedure AddVATAllocationBasis(var AmountByVATKey: Dictionary of [Text, Decimal]; var VATRateByKey: Dictionary of [Text, Decimal]; var VATCategoryByKey: Dictionary of [Text, Text]; var VATKeys: List of [Text]; VATKey: Text; VATRate: Decimal; VATCategoryCode: Text; GrossAmount: Decimal) + begin + if AmountByVATKey.ContainsKey(VATKey) then begin + AmountByVATKey.Set(VATKey, AmountByVATKey.Get(VATKey) + GrossAmount); + exit; + end; + + AmountByVATKey.Add(VATKey, GrossAmount); + VATRateByKey.Add(VATKey, VATRate); + VATCategoryByKey.Add(VATKey, VATCategoryCode); + InsertVATKeySorted(VATKeys, VATKey); + end; + + local procedure InsertVATKeySorted(var VATKeys: List of [Text]; VATKey: Text) + var + ExistingVATKey: Text; + Index: Integer; + begin + for Index := 1 to VATKeys.Count() do begin + VATKeys.Get(Index, ExistingVATKey); + if VATKey < ExistingVATKey then begin + VATKeys.Insert(Index, VATKey); + exit; + end; + end; + VATKeys.Add(VATKey); + end; + + local procedure InsertAllocatedVATAmounts(FREInvoiceMessage: Record "FR E-Invoice Message"; AmountByVATKey: Dictionary of [Text, Decimal]; VATRateByKey: Dictionary of [Text, Decimal]; VATCategoryByKey: Dictionary of [Text, Text]; VATKeys: List of [Text]; EligibleGrossAmount: Decimal; CurrencyCode: Code[10]) + var + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + AllocatedAmount: Decimal; + RemainingAmount: Decimal; + RoundingPrecision: Decimal; + VATKey: Text; + LineNo: Integer; + begin + RoundingPrecision := GetAmountRoundingPrecision(CurrencyCode); + RemainingAmount := FREInvoiceMessage.Amount; + foreach VATKey in VATKeys do begin + LineNo += 10000; + if LineNo div 10000 = VATKeys.Count() then + AllocatedAmount := RemainingAmount + else begin + AllocatedAmount := Round( + FREInvoiceMessage.Amount * AmountByVATKey.Get(VATKey) / EligibleGrossAmount, + RoundingPrecision); + RemainingAmount -= AllocatedAmount; + end; + InsertVATBreakdown( + FREInvoiceMessageVAT, FREInvoiceMessage."Entry No.", LineNo, + VATRateByKey.Get(VATKey), VATCategoryByKey.Get(VATKey), AllocatedAmount, CurrencyCode); + end; + end; + + local procedure CreateReversalVATBreakdown(FREInvoiceMessage: Record "FR E-Invoice Message"; OriginalEntryNo: Integer) + var + OriginalMessageVAT: Record "FR E-Invoice Message VAT"; + ReversalMessageVAT: Record "FR E-Invoice Message VAT"; + begin + OriginalMessageVAT.SetRange("Message Entry No.", OriginalEntryNo); + if not OriginalMessageVAT.FindSet() then + Error(OriginalVATBreakdownErr, OriginalEntryNo); + + repeat + InsertVATBreakdown( + ReversalMessageVAT, FREInvoiceMessage."Entry No.", OriginalMessageVAT."Line No.", + OriginalMessageVAT."VAT %", OriginalMessageVAT."VAT Category Code", + -OriginalMessageVAT.Amount, FREInvoiceMessage."Currency Code"); + until OriginalMessageVAT.Next() = 0; + end; + + local procedure InsertVATBreakdown(var FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; MessageEntryNo: Integer; LineNo: Integer; VATRate: Decimal; VATCategoryCode: Text; Amount: Decimal; CurrencyCode: Code[10]) + begin + FREInvoiceMessageVAT.Init(); + FREInvoiceMessageVAT."Message Entry No." := MessageEntryNo; + FREInvoiceMessageVAT."Line No." := LineNo; + FREInvoiceMessageVAT."VAT %" := VATRate; + FREInvoiceMessageVAT."VAT Category Code" := CopyStr(VATCategoryCode, 1, MaxStrLen(FREInvoiceMessageVAT."VAT Category Code")); + FREInvoiceMessageVAT.Amount := Amount; + FREInvoiceMessageVAT."Currency Code" := CurrencyCode; + FREInvoiceMessageVAT.Insert(); + end; + + local procedure GetAmountRoundingPrecision(CurrencyCode: Code[10]): Decimal + var + Currency: Record Currency; + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + GeneralLedgerSetup.Get(); + if CurrencyCode = GeneralLedgerSetup."LCY Code" then + exit(GeneralLedgerSetup."Amount Rounding Precision"); + + Currency.Get(CurrencyCode); + Currency.TestField("Amount Rounding Precision"); + exit(Currency."Amount Rounding Precision"); + end; + + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyCode <> '' then + exit(CurrencyCode); + + GeneralLedgerSetup.Get(); + GeneralLedgerSetup.TestField("LCY Code"); + exit(GeneralLedgerSetup."LCY Code"); + end; + local procedure IsEligibleFrenchEDocument(EDocument: Record "E-Document"): Boolean var EDocumentService: Record "E-Document Service"; @@ -127,6 +343,21 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]); end; + local procedure IsCollectedReportingRequired(EDocument: Record "E-Document"; DetailedLedgerEntryNo: Integer): Boolean + var + VATEntry: Record "VAT Entry"; + begin + FindInvoiceVATEntries(VATEntry, EDocument, DetailedLedgerEntryNo); + VATEntry.SetLoadFields("VAT Calculation Type", "Tax Jurisdiction Code", "VAT Bus. Posting Group", "VAT Prod. Posting Group", "Unrealized Amount", "Unrealized Base"); + if VATEntry.FindSet() then + repeat + if IsVATEntryReportable(VATEntry) then + exit(true); + until VATEntry.Next() = 0; + + exit(false); + end; + local procedure CheckBuyerResponseAllowed(EDocument: Record "E-Document") var FREInvoiceMessage: Record "FR E-Invoice Message"; @@ -157,4 +388,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." ReasonCodeRequiredErr: Label 'A refusal reason code is required.'; ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.'; AlreadyRespondedErr: Label 'Invoice %1 already has a buyer response.', Comment = '%1 = invoice number'; + VATBreakdownErr: Label 'A reportable VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; + VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; + OriginalVATBreakdownErr: Label 'The VAT breakdown for original French invoice message %1 does not exist.', Comment = '%1 = French invoice message entry number'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al new file mode 100644 index 00000000000..4b6fcb4336e --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al @@ -0,0 +1,82 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +table 10971 "FR E-Invoice Message VAT" +{ + Caption = 'FR E-Invoice Message VAT'; + DataClassification = CustomerContent; + InherentEntitlements = X; + InherentPermissions = X; + ReplicateData = false; + + fields + { + field(1; "Message Entry No."; Integer) + { + Caption = 'Message Entry No.'; + DataClassification = SystemMetadata; + TableRelation = "FR E-Invoice Message"."Entry No."; + } + field(2; "Line No."; Integer) + { + Caption = 'Line No.'; + DataClassification = SystemMetadata; + } + field(3; "VAT %"; Decimal) + { + Caption = 'VAT %'; + DataClassification = CustomerContent; + DecimalPlaces = 0 : 5; + } + field(4; "VAT Category Code"; Code[10]) + { + Caption = 'VAT Category Code'; + DataClassification = CustomerContent; + } + field(5; Amount; Decimal) + { + AutoFormatExpression = Rec."Currency Code"; + AutoFormatType = 1; + Caption = 'Amount'; + DataClassification = CustomerContent; + } + field(6; "Currency Code"; Code[10]) + { + Caption = 'Currency Code'; + DataClassification = CustomerContent; + } + } + + keys + { + key(PK; "Message Entry No.", "Line No.") + { + Clustered = true; + } + key(VATBreakdown; "Message Entry No.", "VAT %", "VAT Category Code") + { + Unique = true; + } + } + + trigger OnModify() + begin + Error(ImmutableVATBreakdownErr); + end; + + trigger OnDelete() + begin + Error(ImmutableVATBreakdownErr); + end; + + trigger OnRename() + begin + Error(ImmutableVATBreakdownErr); + end; + + var + ImmutableVATBreakdownErr: Label 'A French electronic invoice message VAT breakdown cannot be changed.'; +} diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 35ef0201902..c7f7b79438a 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; +using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Enums; using Microsoft.Sales.Customer; @@ -28,6 +29,8 @@ codeunit 148151 "FR E-Invoice Message Tests" tabledata "E-Document Service Status" = rimd, tabledata "E-Doc. Payment Occurrence" = rimd, tabledata "FR E-Invoice Message" = rimd, + tabledata "FR E-Invoice Message VAT" = r, + tabledata "General Ledger Setup" = rm, tabledata "Sales Invoice Header" = rimd; var @@ -63,13 +66,13 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); Assert.RecordCount(EDocPaymentOccurrence, 1); EDocPaymentOccurrence.FindFirst(); - Assert.AreEqual(100, EDocPaymentOccurrence.Amount, 'The generic applied occurrence must carry a positive amount.'); + Assert.AreEqual(120, EDocPaymentOccurrence.Amount, 'The generic applied occurrence must carry a positive amount.'); Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Payment posting must queue the message without invoking the connector.'); FREInvoiceMessage.FindFirst(); SendMessage(FREInvoiceMessage); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '212'); - AssertPayloadAmount(MessageSenderMock.GetLastPayload(), 100, 'EUR'); + AssertPayloadAmount(MessageSenderMock.GetLastPayload(), 120, 'EUR'); AssertPayloadDateFormat(MessageSenderMock.GetLastPayload(), '204'); end; @@ -144,7 +147,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); CollectedMessage.FindFirst(); SendMessage(CollectedMessage); - CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -100); + CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); @@ -164,7 +167,7 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'Unapplication must queue the reversal without invoking the connector.'); SendMessage(NegativeMessage); Assert.AreEqual(2, MessageSenderMock.GetSendCount(), 'Collected and Negative Collected messages must be sent.'); - AssertPayloadAmount(MessageSenderMock.GetLastPayload(), -100, 'EUR'); + AssertPayloadAmount(MessageSenderMock.GetLastPayload(), -120, 'EUR'); end; [Test] @@ -275,6 +278,241 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.RecordCount(FREInvoiceMessage, 1); end; + [Test] + procedure SingleRateFullPaymentCreatesOneVATRow() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Single-rate full payment creates one frozen VAT row summing to reportable amount + Initialize(); + + // [GIVEN] An approved French E-Document with unrealized VAT at 20% and a full payment applied + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] One VAT row is created with amount equal to the message amount and the XML includes amount and rate + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + Assert.RecordCount(FREInvoiceMessageVAT, 1); + FREInvoiceMessageVAT.FindFirst(); + Assert.AreEqual(FREInvoiceMessage.Amount, FREInvoiceMessageVAT.Amount, 'VAT row amount must equal message amount.'); + Assert.AreEqual(20, FREInvoiceMessageVAT."VAT %", 'VAT rate must match the posting setup.'); + Assert.AreEqual('S', Format(FREInvoiceMessageVAT."VAT Category Code"), 'VAT category must be standard.'); + SendMessage(FREInvoiceMessage); + AssertPayloadVATCharacteristic(MessageSenderMock.GetLastPayload(), FREInvoiceMessage.Amount, 20); + end; + + [Test] + procedure SingleRatePartialPaymentAllocatesPartialAmount() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Single-rate partial payment allocates partial amount + Initialize(); + + // [GIVEN] An approved French E-Document with unrealized VAT at 20% and a partial payment of 60 applied + CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved, 60); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The message amount and VAT row reflect the partial payment + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + Assert.AreEqual(60, FREInvoiceMessage.Amount, 'Message amount must equal the partial payment.'); + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + Assert.RecordCount(FREInvoiceMessageVAT, 1); + FREInvoiceMessageVAT.FindFirst(); + Assert.AreEqual(60, FREInvoiceMessageVAT.Amount, 'VAT row must carry the full partial payment.'); + end; + + [Test] + procedure MixedInvoiceReportsProportionalEligibleShare() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Mixed invoice with unrealized and realized VAT reports only proportional eligible share + Initialize(); + + // [GIVEN] An invoice with one unrealized-VAT line (20%, gross 120) and one realized-VAT line (10%, gross 110), full payment of 230 + CreateMixedVATPaymentScenario(EDocument, DetailedCustLedgEntry); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The message amount reflects only the eligible gross share + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + Assert.AreEqual(120, FREInvoiceMessage.Amount, 'Amount must reflect only the eligible gross share.'); + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + Assert.RecordCount(FREInvoiceMessageVAT, 1); + FREInvoiceMessageVAT.FindFirst(); + Assert.AreEqual(20, FREInvoiceMessageVAT."VAT %", 'Only the unrealized VAT rate must appear.'); + end; + + [Test] + procedure MultiRatePaymentWithRoundingStoresExactSum() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + VATRowSum: Decimal; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Multiple eligible VAT rates with rounding residue sum exactly to message amount + Initialize(); + + // [GIVEN] An invoice with three unrealized VAT rates (10%, 20%, 7%) and a partial payment of 99 causing rounding residue + CreateMultiRatePaymentScenario(EDocument, DetailedCustLedgEntry); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The sum of VAT rows equals the message amount deterministically + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + Assert.RecordCount(FREInvoiceMessageVAT, 3); + FREInvoiceMessageVAT.FindSet(); + repeat + VATRowSum += FREInvoiceMessageVAT.Amount; + until FREInvoiceMessageVAT.Next() = 0; + Assert.AreEqual(FREInvoiceMessage.Amount, VATRowSum, 'Sum of VAT rows must equal message amount deterministically.'); + end; + + [Test] + procedure InvoiceWithoutUnrealizedVATCreatesNoCollected() + var + EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Invoice without unrealized VAT keeps generic payment occurrence but creates no FR Collected message + Initialize(); + + // [GIVEN] An approved French E-Document with ordinary (non-unrealized) VAT and an applied payment + CreateNormalVATPaymentScenario(EDocument, DetailedCustLedgEntry); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] A generic payment occurrence exists but no Collected message is created + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 0); + end; + + [Test] + procedure ReversalCopiesFrozenRowsWithNegatedValues() + var + EDocument: Record "E-Document"; + CollectedMessage: Record "FR E-Invoice Message"; + NegativeMessage: Record "FR E-Invoice Message"; + OriginalVAT: Record "FR E-Invoice Message VAT"; + ReversalVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + VATPostingSetup: Record "VAT Posting Setup"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Reversal copies original frozen rows with negated values even if VAT setup changes after original + Initialize(); + + // [GIVEN] A collected message with a frozen VAT breakdown + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); + CollectedMessage.FindFirst(); + SendMessage(CollectedMessage); + OriginalVAT.SetRange("Message Entry No.", CollectedMessage."Entry No."); + OriginalVAT.FindFirst(); + + // [GIVEN] The VAT posting setup category is changed after the original message + VATPostingSetup.SetRange("Tax Category", 'S'); + VATPostingSetup.SetRange("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::Percentage); + VATPostingSetup.FindFirst(); + VATPostingSetup."Tax Category" := 'Z'; + VATPostingSetup.Modify(); + + // [WHEN] The payment is unapplied + CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); + FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + + // [THEN] The reversal has the original frozen rate and negated amount + NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + NegativeMessage.SetRange(Type, NegativeMessage.Type::"Negative Collected"); + NegativeMessage.FindFirst(); + Assert.AreEqual(-CollectedMessage.Amount, NegativeMessage.Amount, 'Reversal amount must negate the original.'); + Assert.IsTrue(NegativeMessage.Amount < 0, 'Reversal message amount must be negative.'); + ReversalVAT.SetRange("Message Entry No.", NegativeMessage."Entry No."); + ReversalVAT.FindFirst(); + Assert.AreEqual(-OriginalVAT.Amount, ReversalVAT.Amount, 'Reversal VAT amount must negate the original.'); + Assert.AreEqual(OriginalVAT."VAT %", ReversalVAT."VAT %", 'Reversal must use frozen original rate not current setup.'); + Assert.AreEqual('S', Format(OriginalVAT."VAT Category Code"), 'Original category must be the original value.'); + Assert.AreEqual(OriginalVAT."VAT Category Code", ReversalVAT."VAT Category Code", 'Reversal must preserve frozen original category.'); + end; + + [Test] + procedure ReplayDoesNotDuplicateAllocationRows() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] Replay is idempotent and does not duplicate allocation rows + Initialize(); + + // [GIVEN] An approved French E-Document with unrealized VAT + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + + // [WHEN] The payment application is processed twice + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] Only one message and one VAT row exist + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 1); + FREInvoiceMessage.FindFirst(); + FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); + Assert.RecordCount(FREInvoiceMessageVAT, 1); + end; + [Test] procedure IncomingMessageIsCorrelatedAndDeduplicated() var @@ -734,15 +972,38 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(ExpectedReasonCode, ReasonCodeNode.AsXmlElement().InnerText(), 'The payload reason code is incorrect.'); end; + local procedure AssertPayloadVATCharacteristic(Payload: Text; ExpectedAmount: Decimal; ExpectedVATRate: Decimal) + var + XmlDoc: XmlDocument; + AmountNode: XmlNode; + RateNode: XmlNode; + ActualAmount: Decimal; + ActualRate: Decimal; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValueAmount"]', AmountNode), 'The payload must contain a value amount.'); + Assert.IsTrue(Evaluate(ActualAmount, AmountNode.AsXmlElement().InnerText(), 9), 'The characteristic amount must be valid.'); + Assert.AreEqual(ExpectedAmount, ActualAmount, 'The characteristic amount is incorrect.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ValuePercent"]', RateNode), 'The payload must contain a value percent.'); + Assert.IsTrue(Evaluate(ActualRate, RateNode.AsXmlElement().InnerText(), 9), 'The characteristic rate must be valid.'); + Assert.AreEqual(ExpectedVATRate, ActualRate, 'The characteristic VAT rate is incorrect.'); + end; + local procedure Initialize() var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; + GeneralLedgerSetup: Record "General Ledger Setup"; begin EDocPaymentOccurrence.DeleteAll(); FREInvoiceMessage.DeleteAll(); MessageSenderMock.Reset(); EnsureService(); + GeneralLedgerSetup.Get(); + if not GeneralLedgerSetup."Unrealized VAT" then begin + GeneralLedgerSetup."Unrealized VAT" := true; + GeneralLedgerSetup.Modify(); + end; end; local procedure EnsureService() @@ -779,6 +1040,11 @@ codeunit 148151 "FR E-Invoice Message Tests" end; local procedure CreatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status") + begin + CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, ServiceStatus, 120); + end; + + local procedure CreatePaymentScenarioWithAmount(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status"; PaymentAmount: Decimal) var Customer: Record Customer; EDocumentService: Record "E-Document Service"; @@ -793,7 +1059,11 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocumentService.Get('FR-MESSAGE-MOCK'); EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; EDocumentService.Modify(); - LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 0); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); + VATPostingSetup."Unrealized VAT Type" := VATPostingSetup."Unrealized VAT Type"::Percentage; + VATPostingSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + VATPostingSetup."Tax Category" := 'S'; + VATPostingSetup.Modify(true); LibrarySales.CreateCustomer(Customer); Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); Customer.Modify(true); @@ -812,6 +1082,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument.Init(); EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; @@ -823,12 +1094,208 @@ codeunit 148151 "FR E-Invoice Message Tests" LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, GenJournalBatch."Journal Template Name", GenJournalBatch.Name, GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", - GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -100); + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -PaymentAmount); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + + FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + end; + + local procedure CreateMixedVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesLine: Record "Sales Line"; + UnrealizedVATSetup: Record "VAT Posting Setup"; + NormalVATSetup: Record "VAT Posting Setup"; + PostedInvoiceNo: Code[20]; + begin + EDocumentService.Get('FR-MESSAGE-MOCK'); + EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + EDocumentService.Modify(); + LibraryERM.CreateVATPostingSetupWithAccounts(UnrealizedVATSetup, UnrealizedVATSetup."VAT Calculation Type"::"Normal VAT", 20); + UnrealizedVATSetup."Unrealized VAT Type" := UnrealizedVATSetup."Unrealized VAT Type"::Percentage; + UnrealizedVATSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + UnrealizedVATSetup."Tax Category" := 'S'; + UnrealizedVATSetup.Modify(true); + LibraryERM.CreateVATPostingSetupWithAccounts(NormalVATSetup, NormalVATSetup."VAT Calculation Type"::"Normal VAT", 10); + NormalVATSetup.Rename(UnrealizedVATSetup."VAT Bus. Posting Group", NormalVATSetup."VAT Prod. Posting Group"); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("VAT Bus. Posting Group", UnrealizedVATSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(UnrealizedVATSetup, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(NormalVATSetup, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService.Modify(); + SalesInvoiceHeader.Get(PostedInvoiceNo); + EDocument.Init(); + EDocument."Document No." := PostedInvoiceNo; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); + + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, + GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -230); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + end; + + local procedure CreateMultiRatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesLine: Record "Sales Line"; + VATSetup10: Record "VAT Posting Setup"; + VATSetup20: Record "VAT Posting Setup"; + VATSetup7: Record "VAT Posting Setup"; + PostedInvoiceNo: Code[20]; + begin + EDocumentService.Get('FR-MESSAGE-MOCK'); + EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + EDocumentService.Modify(); + LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup20, VATSetup20."VAT Calculation Type"::"Normal VAT", 20); + VATSetup20."Unrealized VAT Type" := VATSetup20."Unrealized VAT Type"::Percentage; + VATSetup20."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + VATSetup20."Tax Category" := 'S'; + VATSetup20.Modify(true); + LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup10, VATSetup10."VAT Calculation Type"::"Normal VAT", 10); + VATSetup10.Rename(VATSetup20."VAT Bus. Posting Group", VATSetup10."VAT Prod. Posting Group"); + VATSetup10."Unrealized VAT Type" := VATSetup10."Unrealized VAT Type"::Percentage; + VATSetup10."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + VATSetup10."Tax Category" := 'S'; + VATSetup10.Modify(true); + LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup7, VATSetup7."VAT Calculation Type"::"Normal VAT", 7); + VATSetup7.Rename(VATSetup20."VAT Bus. Posting Group", VATSetup7."VAT Prod. Posting Group"); + VATSetup7."Unrealized VAT Type" := VATSetup7."Unrealized VAT Type"::Percentage; + VATSetup7."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + VATSetup7."Tax Category" := 'S'; + VATSetup7.Modify(true); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("VAT Bus. Posting Group", VATSetup20."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup20, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup10, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup7, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService.Modify(); + SalesInvoiceHeader.Get(PostedInvoiceNo); + EDocument.Init(); + EDocument."Document No." := PostedInvoiceNo; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); + + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, + GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -99); GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); GenJournalLine.Modify(true); LibraryERM.PostGeneralJnlLine(GenJournalLine); + FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + end; + local procedure CreateNormalVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesLine: Record "Sales Line"; + VATPostingSetup: Record "VAT Posting Setup"; + PostedInvoiceNo: Code[20]; + begin + EDocumentService.Get('FR-MESSAGE-MOCK'); + EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + EDocumentService.Modify(); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 10); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + Customer.Modify(true); + + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService.Modify(); + SalesInvoiceHeader.Get(PostedInvoiceNo); + EDocument.Init(); + EDocument."Document No." := PostedInvoiceNo; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Insert(); + CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); + + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, + GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -110); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); end; From 7a454d74b71f12c2c0b43a574a7039b4981aa937 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 12:31:01 +0200 Subject: [PATCH 11/49] reason code upd --- .../Core/FREInvoiceMessageBuilder.Codeunit.al | 12 +++-- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 7 --- .../src/FREInvoiceMessageTests.Codeunit.al | 48 ++++++++----------- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index 745f0583ebd..d394802b8f5 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -48,10 +48,14 @@ codeunit 10976 "FR E-Invoice Message Builder" begin ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, RefusedStatusCodeTok)); ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, RefusedStatusNameTok)); - StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); - StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); - StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); - ReferenceElement.Add(StatusElement); + if (FREInvoiceMessage."Reason Code" <> '') or (FREInvoiceMessage."Reason Description" <> '') then begin + StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); + if FREInvoiceMessage."Reason Code" <> '' then + StatusElement.Add(XmlElement.Create('ReasonCode', RamNamespaceTok, FREInvoiceMessage."Reason Code")); + if FREInvoiceMessage."Reason Description" <> '' then + StatusElement.Add(XmlElement.Create('Reason', RamNamespaceTok, FREInvoiceMessage."Reason Description")); + ReferenceElement.Add(StatusElement); + end; end; FREInvoiceMessage.Type::Collected, FREInvoiceMessage.Type::"Negative Collected": diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 97989b107f3..d948acfa150 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -30,11 +30,6 @@ codeunit 10975 "FR E-Invoice Message Mgt." internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500]) begin CheckBuyerResponseAllowed(EDocument); - if ReasonCode = '' then - Error(ReasonCodeRequiredErr); - if ReasonDescription = '' then - Error(ReasonDescriptionRequiredErr); - CreateAndSendMessage(EDocument, "FR E-Invoice Message Type"::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription); end; @@ -385,8 +380,6 @@ codeunit 10975 "FR E-Invoice Message Mgt." end; var - ReasonCodeRequiredErr: Label 'A refusal reason code is required.'; - ReasonDescriptionRequiredErr: Label 'A refusal reason description is required.'; AlreadyRespondedErr: Label 'Invoice %1 already has a buyer response.', Comment = '%1 = invoice number'; VATBreakdownErr: Label 'A reportable VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index c7f7b79438a..073478e14da 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -190,46 +190,25 @@ codeunit 148151 "FR E-Invoice Message Tests" end; [Test] - procedure RefusalRequiresReasonAndCannotBeRepeated() - var - EDocument: Record "E-Document"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; - begin - Initialize(); - CreateIncomingEDocument(EDocument); - - asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', 'Not accepted.'); - Assert.ExpectedError('A refusal reason code is required.'); - Assert.ExpectedErrorCode('Dialog'); - Clear(EDocument); - CreateIncomingEDocument(EDocument); - FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); - SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); - asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Again.'); - Assert.ExpectedError('already has a buyer response'); - Assert.ExpectedErrorCode('Dialog'); - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'A duplicate refusal must not be sent.'); - end; - - [Test] - procedure RefusalRequiresReasonDescription() + procedure RefusalWithoutReasonSendsStatusWithoutReasonElements() var EDocument: Record "E-Document"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] - // [SCENARIO] A buyer refusal requires a reason description + // [SCENARIO] A buyer can refuse an invoice without providing a reason Initialize(); // [GIVEN] An incoming French purchase invoice CreateIncomingEDocument(EDocument); - // [WHEN] The invoice is refused without a reason description - asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', ''); + // [WHEN] The invoice is refused without a reason code or description + FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', ''); + SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); - // [THEN] The refusal is rejected - Assert.ExpectedError('A refusal reason description is required.'); - Assert.ExpectedErrorCode('Dialog'); + // [THEN] The refusal status is sent without empty reason elements + AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '210'); + AssertPayloadHasNoReason(MessageSenderMock.GetLastPayload()); end; [Test] @@ -972,6 +951,17 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(ExpectedReasonCode, ReasonCodeNode.AsXmlElement().InnerText(), 'The payload reason code is incorrect.'); end; + local procedure AssertPayloadHasNoReason(Payload: Text) + var + XmlDoc: XmlDocument; + ReasonNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="SpecifiedDocumentStatus"]', ReasonNode), 'The payload must not contain a document status when no refusal reason is provided.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="ReasonCode"]', ReasonNode), 'The payload must not contain an empty reason code.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="Reason"]', ReasonNode), 'The payload must not contain an empty reason description.'); + end; + local procedure AssertPayloadVATCharacteristic(Payload: Text; ExpectedAmount: Decimal; ExpectedVATRate: Decimal) var XmlDoc: XmlDocument; From dc578d299b5486b4d8e7b5aad9a8cfd21993dd7c Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 12:54:43 +0200 Subject: [PATCH 12/49] Task 647421 --- .../app/src/Core/FREInvoiceMessage.Table.al | 15 +++ .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 28 ++++++ .../app/src/Core/FREInvoiceMessages.Page.al | 15 +++ .../EReportingEDocService.PageExt.al | 35 +++++++ .../EReportingEDocService.TableExt.al | 33 +++++++ .../src/FREInvoiceMessageTests.Codeunit.al | 91 +++++++++++++++++-- 6 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 64911991e0c..e920637fb00 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -96,6 +96,21 @@ table 10970 "FR E-Invoice Message" Caption = 'Received At'; DataClassification = SystemMetadata; } + field(16; "Sender Platform ID"; Text[50]) + { + Caption = 'Sender Platform ID'; + DataClassification = OrganizationIdentifiableInformation; + } + field(17; "Sender Platform Scheme"; Code[4]) + { + Caption = 'Sender Platform Scheme'; + DataClassification = SystemMetadata; + } + field(18; "Sender Platform Name"; Text[100]) + { + Caption = 'Sender Platform Name'; + DataClassification = OrganizationIdentifiableInformation; + } } keys diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index d948acfa150..47ad4264530 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -108,6 +108,12 @@ codeunit 10975 "FR E-Invoice Message Mgt." FREInvoiceMessage."Reason Code" := ReasonCode; FREInvoiceMessage."Reason Description" := ReasonDescription; FREInvoiceMessage."Created At" := CurrentDateTime(); + case MessageType of + MessageType::Collected: + FreezeSenderPlatform(EDocument, FREInvoiceMessage); + MessageType::"Negative Collected": + CopySenderPlatform(FREInvoiceMessage, OriginalEntryNo); + end; FREInvoiceMessage.Insert(); case MessageType of @@ -124,6 +130,28 @@ codeunit 10975 "FR E-Invoice Message Mgt." EDocumentMessageAPI.QueueMessage(FREInvoiceMessage."E-Document Message Entry No."); end; + local procedure FreezeSenderPlatform(EDocument: Record "E-Document"; var FREInvoiceMessage: Record "FR E-Invoice Message") + var + EDocumentService: Record "E-Document Service"; + begin + EDocumentService.Get(EDocument.Service); + if EDocumentService."FR Sender Platform ID" <> '' then + EDocumentService.TestField("FR Sender Platform Scheme"); + FREInvoiceMessage."Sender Platform ID" := EDocumentService."FR Sender Platform ID"; + FREInvoiceMessage."Sender Platform Scheme" := EDocumentService."FR Sender Platform Scheme"; + FREInvoiceMessage."Sender Platform Name" := EDocumentService."FR Sender Platform Name"; + end; + + local procedure CopySenderPlatform(var FREInvoiceMessage: Record "FR E-Invoice Message"; OriginalEntryNo: Integer) + var + OriginalFREInvoiceMessage: Record "FR E-Invoice Message"; + begin + OriginalFREInvoiceMessage.Get(OriginalEntryNo); + FREInvoiceMessage."Sender Platform ID" := OriginalFREInvoiceMessage."Sender Platform ID"; + FREInvoiceMessage."Sender Platform Scheme" := OriginalFREInvoiceMessage."Sender Platform Scheme"; + FREInvoiceMessage."Sender Platform Name" := OriginalFREInvoiceMessage."Sender Platform Name"; + end; + local procedure CreateCollectedVATBreakdown(EDocument: Record "E-Document"; var FREInvoiceMessage: Record "FR E-Invoice Message") var VATEntry: Record "VAT Entry"; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index 697053d4b3c..5a9fd440091 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -78,6 +78,21 @@ page 10973 "FR E-Invoice Messages" ApplicationArea = Basic, Suite; ToolTip = 'Specifies when the incoming lifecycle message was received.'; } + field("Sender Platform ID"; Rec."Sender Platform ID") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen identifier of the sender platform.'; + } + field("Sender Platform Scheme"; Rec."Sender Platform Scheme") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen identifier scheme of the sender platform.'; + } + field("Sender Platform Name"; Rec."Sender Platform Name") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen name of the sender platform.'; + } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al new file mode 100644 index 00000000000..68120403a9f --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.PageExt.al @@ -0,0 +1,35 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; + +pageextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" +{ + layout + { + addlast(ExportProcessing) + { + group(FrenchLifecycle) + { + Caption = 'French Invoice Lifecycle'; + Visible = (Rec."Document Format" = Rec."Document Format"::"Peppol BIS 3.0 FR") or (Rec."Document Format" = Rec."Document Format"::"Factur-X FR"); + + field("FR Sender Platform ID"; Rec."FR Sender Platform ID") + { + ApplicationArea = Basic, Suite; + } + field("FR Sender Platform Scheme"; Rec."FR Sender Platform Scheme") + { + ApplicationArea = Basic, Suite; + } + field("FR Sender Platform Name"; Rec."FR Sender Platform Name") + { + ApplicationArea = Basic, Suite; + } + } + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al new file mode 100644 index 00000000000..52d0ad0a4d0 --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocService.TableExt.al @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +using Microsoft.eServices.EDocument; + +tableextension 10977 "E-Reporting E-Doc. Service" extends "E-Document Service" +{ + fields + { + field(10970; "FR Sender Platform ID"; Text[50]) + { + Caption = 'FR Sender Platform ID'; + DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the identifier of the French approved platform that sends lifecycle messages.'; + } + field(10971; "FR Sender Platform Scheme"; Code[4]) + { + Caption = 'FR Sender Platform Scheme'; + DataClassification = SystemMetadata; + InitValue = '0238'; + ToolTip = 'Specifies the identifier scheme of the French approved platform.'; + } + field(10972; "FR Sender Platform Name"; Text[100]) + { + Caption = 'FR Sender Platform Name'; + DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the name of the French approved platform that sends lifecycle messages.'; + } + } +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 073478e14da..3b999ee6901 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -257,6 +257,68 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.RecordCount(FREInvoiceMessage, 1); end; + [Test] + procedure CollectedMessageFreezesSenderPlatform() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] A Collected message retains the sender-platform identity captured from its service + Initialize(); + + // [GIVEN] An eligible payment and a French service with sender-platform identity + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + + // [WHEN] The payment is processed and the service identity is subsequently changed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + EDocumentService.Get(EDocument.Service); + EDocumentService."FR Sender Platform ID" := 'CHANGED-PLATFORM'; + EDocumentService."FR Sender Platform Scheme" := '9999'; + EDocumentService."FR Sender Platform Name" := 'Changed Platform'; + EDocumentService.Modify(); + + // [THEN] The message retains the original platform values + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + Assert.AreEqual('TEST-PLATFORM', FREInvoiceMessage."Sender Platform ID", 'The sender-platform ID must be frozen at capture.'); + Assert.AreEqual('0238', FREInvoiceMessage."Sender Platform Scheme", 'The sender-platform scheme must be frozen at capture.'); + Assert.AreEqual('Test Platform', FREInvoiceMessage."Sender Platform Name", 'The sender-platform name must be frozen at capture.'); + end; + + [Test] + procedure CollectedMessageAllowsMissingSenderPlatformIdentity() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] A Collected message supports a service without optional sender-platform identity + Initialize(); + + // [GIVEN] An eligible payment whose service has no sender-platform ID + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + EDocumentService.Get(EDocument.Service); + Clear(EDocumentService."FR Sender Platform ID"); + EDocumentService.Modify(); + + // [WHEN] The payment is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The message is created with no frozen platform identity + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + Assert.AreEqual('', FREInvoiceMessage."Sender Platform ID", 'The optional sender-platform ID must remain blank.'); + end; + [Test] procedure SingleRateFullPaymentCreatesOneVATRow() var @@ -415,6 +477,7 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReversalCopiesFrozenRowsWithNegatedValues() var EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; CollectedMessage: Record "FR E-Invoice Message"; NegativeMessage: Record "FR E-Invoice Message"; OriginalVAT: Record "FR E-Invoice Message VAT"; @@ -438,12 +501,17 @@ codeunit 148151 "FR E-Invoice Message Tests" OriginalVAT.SetRange("Message Entry No.", CollectedMessage."Entry No."); OriginalVAT.FindFirst(); - // [GIVEN] The VAT posting setup category is changed after the original message + // [GIVEN] VAT and sender-platform setup are changed after the original message VATPostingSetup.SetRange("Tax Category", 'S'); VATPostingSetup.SetRange("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::Percentage); VATPostingSetup.FindFirst(); VATPostingSetup."Tax Category" := 'Z'; VATPostingSetup.Modify(); + EDocumentService.Get(EDocument.Service); + EDocumentService."FR Sender Platform ID" := 'CHANGED-PLATFORM'; + EDocumentService."FR Sender Platform Scheme" := '9999'; + EDocumentService."FR Sender Platform Name" := 'Changed Platform'; + EDocumentService.Modify(); // [WHEN] The payment is unapplied CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); @@ -461,6 +529,9 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(OriginalVAT."VAT %", ReversalVAT."VAT %", 'Reversal must use frozen original rate not current setup.'); Assert.AreEqual('S', Format(OriginalVAT."VAT Category Code"), 'Original category must be the original value.'); Assert.AreEqual(OriginalVAT."VAT Category Code", ReversalVAT."VAT Category Code", 'Reversal must preserve frozen original category.'); + Assert.AreEqual(CollectedMessage."Sender Platform ID", NegativeMessage."Sender Platform ID", 'Reversal must preserve the original sender-platform ID.'); + Assert.AreEqual(CollectedMessage."Sender Platform Scheme", NegativeMessage."Sender Platform Scheme", 'Reversal must preserve the original sender-platform scheme.'); + Assert.AreEqual(CollectedMessage."Sender Platform Name", NegativeMessage."Sender Platform Name", 'Reversal must preserve the original sender-platform name.'); end; [Test] @@ -1000,13 +1071,17 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocumentService: Record "E-Document Service"; begin - if EDocumentService.Get('FR-MESSAGE-MOCK') then - exit; - EDocumentService.Init(); - EDocumentService.Code := 'FR-MESSAGE-MOCK'; - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"FR Message Mock"; - EDocumentService.Insert(); + if not EDocumentService.Get('FR-MESSAGE-MOCK') then begin + EDocumentService.Init(); + EDocumentService.Code := 'FR-MESSAGE-MOCK'; + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"FR Message Mock"; + EDocumentService.Insert(); + end; + EDocumentService."FR Sender Platform ID" := 'TEST-PLATFORM'; + EDocumentService."FR Sender Platform Scheme" := '0238'; + EDocumentService."FR Sender Platform Name" := 'Test Platform'; + EDocumentService.Modify(); end; local procedure CreateIncomingEDocument(var EDocument: Record "E-Document") From c55c5d4487f4ed0d7174fd63e8ab5d53ee74659b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 13:08:46 +0200 Subject: [PATCH 13/49] retry --- .../app/src/Core/FREInvoiceMessages.Page.al | 1 + .../Message/EDocMessageMgt.Codeunit.al | 16 +++ .../Message/EDocumentMessageAPI.Codeunit.al | 11 ++ .../Message/EDocumentMessagesFactBox.Page.al | 23 ++++ .../EDocMessageMgtTests.Codeunit.al | 109 ++++++++++++++++++ 5 files changed, 160 insertions(+) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index 5a9fd440091..686567f1b68 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -101,4 +101,5 @@ page 10973 "FR E-Invoice Messages" } } } + } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index a0960055650..025c5892ddc 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -169,6 +169,22 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); end; + procedure RetryMessage(MessageEntryNo: Integer) + var + EDocMessage: Record "E-Document Message"; + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; + begin + EDocMessage.Get(MessageEntryNo); + EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); + EDocMessage.TestField(Status, EDocMessage.Status::Error); + EDocMessage.TestField(Service); + + EDocMessage.Status := EDocMessage.Status::Queued; + EDocMessage.Modify(); + Commit(); + EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); + end; + procedure RegisterExternalDocumentReference(EDocument: Record "E-Document"; ServiceCode: Code[20]; ExternalDocumentID: Text[250]) var EDocExternalReference: Record "E-Doc. External Reference"; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al index ad0cbc85a61..969c10e296c 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -97,6 +97,17 @@ codeunit 6532 "E-Document Message API" EDocMessageMgt.QueueMessage(MessageEntryNo); end; + /// + /// Requeues a failed outgoing E-Document message for background transmission using its stored payload. + /// + /// The entry number of the failed E-Document message to retry. + procedure RetryMessage(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.RetryMessage(MessageEntryNo); + end; + /// /// Associates an external service document identifier with an E-Document for later message correlation. /// diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al index 3a3ef8576a8..8de33297620 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al @@ -86,6 +86,23 @@ page 6434 "E-Document Messages FactBox" { area(processing) { + action(Retry) + { + ApplicationArea = Basic, Suite; + Caption = 'Retry'; + ToolTip = 'Requeue the failed message for background transmission using its existing payload.'; + Image = Refresh; + Scope = Repeater; + Enabled = RetryEnabled; + + trigger OnAction() + var + EDocumentMessageAPI: Codeunit "E-Document Message API"; + begin + EDocumentMessageAPI.RetryMessage(Rec."Entry No."); + CurrPage.Update(false); + end; + } action(ViewXML) { ApplicationArea = Basic, Suite; @@ -112,7 +129,13 @@ page 6434 "E-Document Messages FactBox" } } + trigger OnAfterGetCurrRecord() + begin + RetryEnabled := (Rec.Direction = Rec.Direction::Outgoing) and (Rec.Status = Rec.Status::Error); + end; + var + RetryEnabled: Boolean; FileNameTok: Label 'E-Document_%1_Response_%2.xml', Comment = '%1 = E-Document number, %2 = human-readable response type', Locked = true; local procedure BuildFileName(): Text diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 3d1deb9532f..dbea8a46ad7 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -60,6 +60,115 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" Assert.RecordCount(JobQueueEntry, 1); end; + [Test] + procedure RetryMessageRequeuesExistingMessage() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + DataStorageEntryNo: Integer; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO 647423] Retrying a failed outgoing message requeues the existing message without duplication + Initialize(Customer); + + // [GIVEN] A failed outgoing E-Document message with a stored payload + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::Error; + EDocMessage."Last Error" := 'Temporary transport failure'; + EDocMessage.Modify(); + DataStorageEntryNo := EDocMessage."Data Storage Entry No."; + + // [WHEN] The failed message is retried + EDocumentMessageAPI.RetryMessage(MessageEntryNo); + + // [THEN] The same message and payload are queued with one background send job + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual("E-Doc. Message Status"::Queued, EDocMessage.Status, 'The existing message must be requeued.'); + Assert.AreEqual(DataStorageEntryNo, EDocMessage."Data Storage Entry No.", 'Retry must reuse the stored message payload.'); + Assert.RecordCount(EDocMessage, 1); + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Send Job"); + JobQueueEntry.SetRange("Record ID to Process", EDocMessage.RecordId()); + Assert.RecordCount(JobQueueEntry, 1); + end; + + [Test] + procedure RetryMessageRejectsMessageWithoutError() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO 647423] Retry rejects an outgoing message that is not in Error status + Initialize(Customer); + + // [GIVEN] A newly created outgoing E-Document message + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + + // [WHEN] The message is retried + asserterror EDocumentMessageAPI.RetryMessage(MessageEntryNo); + + // [THEN] Retry is rejected because the message has not failed + Assert.ExpectedError('Status must have the value Error'); + end; + + [Test] + procedure RetryMessageRejectsIncomingMessage() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO 647423] Retry rejects a failed incoming message + Initialize(Customer); + + // [GIVEN] A failed incoming E-Document message + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Incoming, + "E-Doc. Response Type"::None, TempBlob); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::Error; + EDocMessage.Modify(); + + // [WHEN] The incoming message is retried + asserterror EDocumentMessageAPI.RetryMessage(MessageEntryNo); + + // [THEN] Retry is rejected because only outgoing messages can be sent + Assert.ExpectedError('Direction must have the value Outgoing'); + end; + local procedure Initialize(var Customer: Record Customer) var EDocument: Record "E-Document"; From 70ca47882b481c54ea98e946b1e405c1bb8303c7 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 13:20:34 +0200 Subject: [PATCH 14/49] PPF context changes --- .../app/src/Core/FREInvoiceMessage.Table.al | 25 +++ .../Core/FREInvoiceMessageBuilder.Codeunit.al | 143 +++++++++++++++++- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 25 ++- .../app/src/Core/FREInvoiceMessages.Page.al | 25 +++ .../src/FREInvoiceMessageTests.Codeunit.al | 137 +++++++++++++++++ 5 files changed, 351 insertions(+), 4 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index e920637fb00..b1538b6d033 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -111,6 +111,31 @@ table 10970 "FR E-Invoice Message" Caption = 'Sender Platform Name'; DataClassification = OrganizationIdentifiableInformation; } + field(19; "Invoice Issue Date"; Date) + { + Caption = 'Invoice Issue Date'; + DataClassification = CustomerContent; + } + field(20; "Invoice Receipt At"; DateTime) + { + Caption = 'Invoice Receipt At'; + DataClassification = CustomerContent; + } + field(21; "Invoice Issuer ID"; Text[50]) + { + Caption = 'Invoice Issuer ID'; + DataClassification = OrganizationIdentifiableInformation; + } + field(22; "Invoice Issuer Scheme"; Code[4]) + { + Caption = 'Invoice Issuer Scheme'; + DataClassification = SystemMetadata; + } + field(23; "Invoice Issuer Name"; Text[100]) + { + Caption = 'Invoice Issuer Name'; + DataClassification = OrganizationIdentifiableInformation; + } } keys diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index d394802b8f5..c3af9705b39 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -24,20 +24,31 @@ codeunit 10976 "FR E-Invoice Message Builder" OutStream: OutStream; begin FREInvoiceMessage.TestField("Event Date"); + if IsPPFMessage(FREInvoiceMessage) then + ValidatePPFContext(FREInvoiceMessage); XmlDoc := XmlDocument.Create(); XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); RootElement := XmlElement.Create('CrossDomainAcknowledgementAndResponse', RsmNamespaceTok); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('qdt', QdtNamespaceTok)); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('rsm', RsmNamespaceTok)); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('udt', UdtNamespaceTok)); - RootElement.Add(XmlElement.Create('ExchangedDocument', RsmNamespaceTok, - XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID")))); + AddExchangedDocumentContext(RootElement, FREInvoiceMessage); + AddExchangedDocument(RootElement, FREInvoiceMessage); AcknowledgementElement := XmlElement.Create('AcknowledgementDocument', RsmNamespaceTok); + AcknowledgementElement.Add(CreateIndicatorElement('MultipleReferencesIndicator', false)); + AcknowledgementElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, InformationTypeCodeTok)); AddIssueDateTime(AcknowledgementElement, FREInvoiceMessage."Event Date"); ReferenceElement := XmlElement.Create('ReferenceReferencedDocument', RamNamespaceTok); ReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, EDocument."Document No.")); ReferenceElement.Add(XmlElement.Create('StatusCode', RamNamespaceTok, InvoiceReferenceStatusCodeTok)); + ReferenceElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, InvoiceTypeCodeTok)); + if IsPPFMessage(FREInvoiceMessage) then begin + ReferenceElement.Add(CreateDateTimeElement('ReceiptDateTime', FREInvoiceMessage."Invoice Receipt At")); + ReferenceElement.Add(XmlElement.Create('ReferenceTypeCode', RamNamespaceTok, PPFInvoiceProfileTok)); + ReferenceElement.Add(CreateFormattedIssueDateTime(FREInvoiceMessage."Invoice Issue Date")); + end; case FREInvoiceMessage.Type of FREInvoiceMessage.Type::Accepted: begin @@ -62,6 +73,10 @@ codeunit 10976 "FR E-Invoice Message Builder" begin ReferenceElement.Add(XmlElement.Create('ProcessConditionCode', RamNamespaceTok, CollectedStatusCodeTok)); ReferenceElement.Add(XmlElement.Create('ProcessCondition', RamNamespaceTok, CollectedStatusNameTok)); + if IsPPFMessage(FREInvoiceMessage) then + ReferenceElement.Add( + CreateTradeParty( + 'IssuerTradeParty', FREInvoiceMessage."Invoice Issuer ID", FREInvoiceMessage."Invoice Issuer Scheme", '', '')); AddVATBreakdown(ReferenceElement, FREInvoiceMessage); end; else @@ -75,6 +90,56 @@ codeunit 10976 "FR E-Invoice Message Builder" XmlDoc.WriteTo(OutStream); end; + local procedure AddExchangedDocumentContext(var RootElement: XmlElement; FREInvoiceMessage: Record "FR E-Invoice Message") + var + BusinessProcessElement: XmlElement; + ContextElement: XmlElement; + GuidelineElement: XmlElement; + begin + ContextElement := XmlElement.Create('ExchangedDocumentContext', RsmNamespaceTok); + if not IsPPFMessage(FREInvoiceMessage) then begin + BusinessProcessElement := XmlElement.Create('BusinessProcessSpecifiedDocumentContextParameter', RamNamespaceTok); + BusinessProcessElement.Add(XmlElement.Create('ID', RamNamespaceTok, RegulatedBusinessProcessTok)); + ContextElement.Add(BusinessProcessElement); + end; + GuidelineElement := XmlElement.Create('GuidelineSpecifiedDocumentContextParameter', RamNamespaceTok); + GuidelineElement.Add(XmlElement.Create('ID', RamNamespaceTok, GetProfileID(FREInvoiceMessage))); + ContextElement.Add(GuidelineElement); + RootElement.Add(ContextElement); + end; + + local procedure AddExchangedDocument(var RootElement: XmlElement; FREInvoiceMessage: Record "FR E-Invoice Message") + var + ExchangedDocumentElement: XmlElement; + IssueDateTimeElement: XmlElement; + begin + ExchangedDocumentElement := XmlElement.Create('ExchangedDocument', RsmNamespaceTok); + ExchangedDocumentElement.Add(XmlElement.Create('ID', RamNamespaceTok, Format(FREInvoiceMessage."Source Occurrence ID"))); + ExchangedDocumentElement.Add(XmlElement.Create('Name', RamNamespaceTok, LifecycleMessageNameTok)); + IssueDateTimeElement := XmlElement.Create('IssueDateTime', RamNamespaceTok); + IssueDateTimeElement.Add(CreateDateTimeString(FREInvoiceMessage."Created At")); + ExchangedDocumentElement.Add(IssueDateTimeElement); + if IsPPFMessage(FREInvoiceMessage) then begin + ExchangedDocumentElement.Add( + CreateTradeParty( + 'SenderTradeParty', FREInvoiceMessage."Sender Platform ID", FREInvoiceMessage."Sender Platform Scheme", + FREInvoiceMessage."Sender Platform Name", SenderRoleCodeTok)); + ExchangedDocumentElement.Add( + CreateTradeParty( + 'IssuerTradeParty', FREInvoiceMessage."Invoice Issuer ID", FREInvoiceMessage."Invoice Issuer Scheme", + FREInvoiceMessage."Invoice Issuer Name", SellerRoleCodeTok)); + ExchangedDocumentElement.Add( + CreateTradeParty('RecipientTradeParty', PPFIdentifierTok, PPFIdentifierSchemeTok, PPFNameTok, PPFRoleCodeTok)); + end; + RootElement.Add(ExchangedDocumentElement); + end; + + local procedure CreateIndicatorElement(ElementName: Text; Value: Boolean) IndicatorElement: XmlElement + begin + IndicatorElement := XmlElement.Create(ElementName, RamNamespaceTok); + IndicatorElement.Add(XmlElement.Create('Indicator', UdtNamespaceTok, Format(Value, 0, 9).ToLower())); + end; + local procedure AddVATBreakdown(var ReferenceElement: XmlElement; FREInvoiceMessage: Record "FR E-Invoice Message") var FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; @@ -121,6 +186,66 @@ codeunit 10976 "FR E-Invoice Message Builder" AcknowledgementElement.Add(IssueDateTimeElement); end; + local procedure CreateDateTimeString(Value: DateTime) DateTimeStringElement: XmlElement + begin + DateTimeStringElement := XmlElement.Create('DateTimeString', UdtNamespaceTok, Format(Value, 0, '')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateTimeFormatCodeTok)); + end; + + local procedure CreateDateTimeElement(ElementName: Text; Value: DateTime) DateTimeElement: XmlElement + begin + DateTimeElement := XmlElement.Create(ElementName, RamNamespaceTok); + DateTimeElement.Add(CreateDateTimeString(Value)); + end; + + local procedure CreateFormattedIssueDateTime(Value: Date) FormattedIssueDateTimeElement: XmlElement + var + DateTimeStringElement: XmlElement; + begin + FormattedIssueDateTimeElement := XmlElement.Create('FormattedIssueDateTime', RamNamespaceTok); + DateTimeStringElement := XmlElement.Create('DateTimeString', QdtNamespaceTok, Format(Value, 0, '')); + DateTimeStringElement.Add(XmlAttribute.Create('format', DateFormatCodeTok)); + FormattedIssueDateTimeElement.Add(DateTimeStringElement); + end; + + local procedure CreateTradeParty(ElementName: Text; Identifier: Text; IdentifierScheme: Text; PartyName: Text; RoleCode: Text) TradePartyElement: XmlElement + var + GlobalIDElement: XmlElement; + begin + TradePartyElement := XmlElement.Create(ElementName, RamNamespaceTok); + GlobalIDElement := XmlElement.Create('GlobalID', RamNamespaceTok, Identifier); + GlobalIDElement.Add(XmlAttribute.Create('schemeID', IdentifierScheme)); + TradePartyElement.Add(GlobalIDElement); + if PartyName <> '' then + TradePartyElement.Add(XmlElement.Create('Name', RamNamespaceTok, PartyName)); + if RoleCode <> '' then + TradePartyElement.Add(XmlElement.Create('RoleCode', RamNamespaceTok, RoleCode)); + end; + + local procedure IsPPFMessage(FREInvoiceMessage: Record "FR E-Invoice Message"): Boolean + begin + exit(FREInvoiceMessage."Sender Platform ID" <> ''); + end; + + local procedure GetProfileID(FREInvoiceMessage: Record "FR E-Invoice Message"): Text + begin + if IsPPFMessage(FREInvoiceMessage) then + exit(PPFInvoiceProfileTok); + exit(CDVInvoiceProfileTok); + end; + + local procedure ValidatePPFContext(FREInvoiceMessage: Record "FR E-Invoice Message") + begin + FREInvoiceMessage.TestField("Invoice Issue Date"); + FREInvoiceMessage.TestField("Invoice Receipt At"); + FREInvoiceMessage.TestField("Sender Platform ID"); + FREInvoiceMessage.TestField("Sender Platform Scheme"); + FREInvoiceMessage.TestField("Sender Platform Name"); + FREInvoiceMessage.TestField("Invoice Issuer ID"); + FREInvoiceMessage.TestField("Invoice Issuer Scheme"); + FREInvoiceMessage.TestField("Invoice Issuer Name"); + end; + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] var GeneralLedgerSetup: Record "General Ledger Setup"; @@ -135,9 +260,17 @@ codeunit 10976 "FR E-Invoice Message Builder" var RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + QdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', Locked = true; UdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', Locked = true; + RegulatedBusinessProcessTok: Label 'REGULATED', Locked = true; + CDVInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:invoice', Locked = true; + PPFInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:einvoicingF2', Locked = true; + LifecycleMessageNameTok: Label 'Invoice lifecycle message', Locked = true; + InformationTypeCodeTok: Label '23', Locked = true; DateTimeFormatCodeTok: Label '204', Locked = true; + DateFormatCodeTok: Label '102', Locked = true; InvoiceReferenceStatusCodeTok: Label '47', Locked = true; + InvoiceTypeCodeTok: Label '380', Locked = true; CollectedStatusCodeTok: Label '212', Locked = true; CollectedStatusNameTok: Label 'Encaissée', Locked = true; CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; @@ -145,6 +278,12 @@ codeunit 10976 "FR E-Invoice Message Builder" RefusedStatusNameTok: Label 'Refusée', Locked = true; AcceptedStatusCodeTok: Label '205', Locked = true; AcceptedStatusNameTok: Label 'Approuvée', Locked = true; + SenderRoleCodeTok: Label 'WK', Locked = true; + SellerRoleCodeTok: Label 'SE', Locked = true; + PPFIdentifierTok: Label '9998', Locked = true; + PPFIdentifierSchemeTok: Label '0238', Locked = true; + PPFNameTok: Label 'PPF', Locked = true; + PPFRoleCodeTok: Label 'DFH', Locked = true; VATBreakdownErr: Label 'French invoice message %1 does not have the VAT breakdown required for a collected status message.', Comment = '%1 = French invoice message entry number'; UnsupportedMessageTypeErr: Label 'French invoice lifecycle message type %1 cannot be sent.', Comment = '%1 = French invoice lifecycle message type'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 47ad4264530..1aa793cb737 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -10,6 +10,7 @@ using Microsoft.Finance.Currency; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.Company; using Microsoft.Sales.Receivables; using System.Utilities; @@ -132,14 +133,28 @@ codeunit 10975 "FR E-Invoice Message Mgt." local procedure FreezeSenderPlatform(EDocument: Record "E-Document"; var FREInvoiceMessage: Record "FR E-Invoice Message") var + CompanyInformation: Record "Company Information"; EDocumentService: Record "E-Document Service"; begin EDocumentService.Get(EDocument.Service); - if EDocumentService."FR Sender Platform ID" <> '' then - EDocumentService.TestField("FR Sender Platform Scheme"); FREInvoiceMessage."Sender Platform ID" := EDocumentService."FR Sender Platform ID"; FREInvoiceMessage."Sender Platform Scheme" := EDocumentService."FR Sender Platform Scheme"; FREInvoiceMessage."Sender Platform Name" := EDocumentService."FR Sender Platform Name"; + if FREInvoiceMessage."Sender Platform ID" = '' then + exit; + + EDocument.TestField("Document Date"); + EDocument.TestField("Clearance Date"); + EDocumentService.TestField("FR Sender Platform Scheme"); + EDocumentService.TestField("FR Sender Platform Name"); + CompanyInformation.Get(); + CompanyInformation.TestField("Registration No."); + CompanyInformation.TestField(Name); + FREInvoiceMessage."Invoice Issue Date" := EDocument."Document Date"; + FREInvoiceMessage."Invoice Receipt At" := EDocument."Clearance Date"; + FREInvoiceMessage."Invoice Issuer ID" := CopyStr(CompanyInformation."Registration No.", 1, 9); + FREInvoiceMessage."Invoice Issuer Scheme" := SIRENSchemeTok; + FREInvoiceMessage."Invoice Issuer Name" := CompanyInformation.Name; end; local procedure CopySenderPlatform(var FREInvoiceMessage: Record "FR E-Invoice Message"; OriginalEntryNo: Integer) @@ -150,6 +165,11 @@ codeunit 10975 "FR E-Invoice Message Mgt." FREInvoiceMessage."Sender Platform ID" := OriginalFREInvoiceMessage."Sender Platform ID"; FREInvoiceMessage."Sender Platform Scheme" := OriginalFREInvoiceMessage."Sender Platform Scheme"; FREInvoiceMessage."Sender Platform Name" := OriginalFREInvoiceMessage."Sender Platform Name"; + FREInvoiceMessage."Invoice Issue Date" := OriginalFREInvoiceMessage."Invoice Issue Date"; + FREInvoiceMessage."Invoice Receipt At" := OriginalFREInvoiceMessage."Invoice Receipt At"; + FREInvoiceMessage."Invoice Issuer ID" := OriginalFREInvoiceMessage."Invoice Issuer ID"; + FREInvoiceMessage."Invoice Issuer Scheme" := OriginalFREInvoiceMessage."Invoice Issuer Scheme"; + FREInvoiceMessage."Invoice Issuer Name" := OriginalFREInvoiceMessage."Invoice Issuer Name"; end; local procedure CreateCollectedVATBreakdown(EDocument: Record "E-Document"; var FREInvoiceMessage: Record "FR E-Invoice Message") @@ -408,6 +428,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." end; var + SIRENSchemeTok: Label '0002', Locked = true; AlreadyRespondedErr: Label 'Invoice %1 already has a buyer response.', Comment = '%1 = invoice number'; VATBreakdownErr: Label 'A reportable VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index 686567f1b68..d02103e2586 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -93,6 +93,31 @@ page 10973 "FR E-Invoice Messages" ApplicationArea = Basic, Suite; ToolTip = 'Specifies the frozen name of the sender platform.'; } + field("Invoice Issue Date"; Rec."Invoice Issue Date") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen issue date of the invoice.'; + } + field("Invoice Receipt At"; Rec."Invoice Receipt At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen date and time when the sender platform received the invoice.'; + } + field("Invoice Issuer ID"; Rec."Invoice Issuer ID") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen SIREN identifier of the invoice issuer.'; + } + field("Invoice Issuer Scheme"; Rec."Invoice Issuer Scheme") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen identifier scheme of the invoice issuer.'; + } + field("Invoice Issuer Name"; Rec."Invoice Issuer Name") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the frozen name of the invoice issuer.'; + } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 3b999ee6901..aaf8103bb05 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -10,6 +10,7 @@ using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Setup; +using Microsoft.Foundation.Company; using Microsoft.Foundation.Enums; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; @@ -31,6 +32,7 @@ codeunit 148151 "FR E-Invoice Message Tests" tabledata "FR E-Invoice Message" = rimd, tabledata "FR E-Invoice Message VAT" = r, tabledata "General Ledger Setup" = rm, + tabledata "Company Information" = rm, tabledata "Sales Invoice Header" = rimd; var @@ -288,6 +290,37 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual('TEST-PLATFORM', FREInvoiceMessage."Sender Platform ID", 'The sender-platform ID must be frozen at capture.'); Assert.AreEqual('0238', FREInvoiceMessage."Sender Platform Scheme", 'The sender-platform scheme must be frozen at capture.'); Assert.AreEqual('Test Platform', FREInvoiceMessage."Sender Platform Name", 'The sender-platform name must be frozen at capture.'); + Assert.AreEqual(EDocument."Document Date", FREInvoiceMessage."Invoice Issue Date", 'The invoice issue date must be frozen at capture.'); + Assert.AreEqual(EDocument."Clearance Date", FREInvoiceMessage."Invoice Receipt At", 'The platform receipt time must be frozen at capture.'); + Assert.AreEqual('123456789', FREInvoiceMessage."Invoice Issuer ID", 'The invoice issuer ID must be frozen at capture.'); + Assert.AreEqual('0002', FREInvoiceMessage."Invoice Issuer Scheme", 'The invoice issuer scheme must identify SIREN.'); + Assert.AreEqual('FR Test Issuer', FREInvoiceMessage."Invoice Issuer Name", 'The invoice issuer name must be frozen at capture.'); + end; + + [Test] + procedure CollectedMessageEmitsCompletePPFContext() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] A Collected message emits the complete PPF platform context + Initialize(); + + // [GIVEN] An eligible payment and a French service with sender-platform identity + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + + // [WHEN] The payment is processed and its lifecycle message is sent + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + SendMessage(FREInvoiceMessage); + + // [THEN] The payload contains the PPF profile, sender, issuer, recipient, and invoice dates + AssertPayloadPPFContext(MessageSenderMock.GetLastPayload(), EDocument); end; [Test] @@ -319,6 +352,36 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual('', FREInvoiceMessage."Sender Platform ID", 'The optional sender-platform ID must remain blank.'); end; + [Test] + procedure CollectedMessageWithoutPlatformUsesCDVProfile() + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO 647421] A Collected message without platform identity retains the CDV profile + Initialize(); + + // [GIVEN] An eligible payment whose service has no sender-platform identity + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + EDocumentService.Get(EDocument.Service); + Clear(EDocumentService."FR Sender Platform ID"); + EDocumentService.Modify(); + + // [WHEN] The payment is processed and its lifecycle message is sent + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + FREInvoiceMessage.FindFirst(); + SendMessage(FREInvoiceMessage); + + // [THEN] The payload uses the CDV profile and does not contain PPF trade parties + AssertPayloadCDVContext(MessageSenderMock.GetLastPayload()); + end; + [Test] procedure SingleRateFullPaymentCreatesOneVATRow() var @@ -476,6 +539,7 @@ codeunit 148151 "FR E-Invoice Message Tests" [Test] procedure ReversalCopiesFrozenRowsWithNegatedValues() var + CompanyInformation: Record "Company Information"; EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; CollectedMessage: Record "FR E-Invoice Message"; @@ -512,6 +576,10 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocumentService."FR Sender Platform Scheme" := '9999'; EDocumentService."FR Sender Platform Name" := 'Changed Platform'; EDocumentService.Modify(); + CompanyInformation.Get(); + CompanyInformation."Registration No." := '987654321'; + CompanyInformation.Name := 'Changed Issuer'; + CompanyInformation.Modify(); // [WHEN] The payment is unapplied CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); @@ -532,6 +600,11 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(CollectedMessage."Sender Platform ID", NegativeMessage."Sender Platform ID", 'Reversal must preserve the original sender-platform ID.'); Assert.AreEqual(CollectedMessage."Sender Platform Scheme", NegativeMessage."Sender Platform Scheme", 'Reversal must preserve the original sender-platform scheme.'); Assert.AreEqual(CollectedMessage."Sender Platform Name", NegativeMessage."Sender Platform Name", 'Reversal must preserve the original sender-platform name.'); + Assert.AreEqual(CollectedMessage."Invoice Issue Date", NegativeMessage."Invoice Issue Date", 'Reversal must preserve the original invoice issue date.'); + Assert.AreEqual(CollectedMessage."Invoice Receipt At", NegativeMessage."Invoice Receipt At", 'Reversal must preserve the original platform receipt time.'); + Assert.AreEqual(CollectedMessage."Invoice Issuer ID", NegativeMessage."Invoice Issuer ID", 'Reversal must preserve the original invoice issuer ID.'); + Assert.AreEqual(CollectedMessage."Invoice Issuer Scheme", NegativeMessage."Invoice Issuer Scheme", 'Reversal must preserve the original invoice issuer scheme.'); + Assert.AreEqual(CollectedMessage."Invoice Issuer Name", NegativeMessage."Invoice Issuer Name", 'Reversal must preserve the original invoice issuer name.'); end; [Test] @@ -1050,6 +1123,51 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(ExpectedVATRate, ActualRate, 'The characteristic VAT rate is incorrect.'); end; + local procedure AssertPayloadPPFContext(Payload: Text; EDocument: Record "E-Document") + var + XmlDoc: XmlDocument; + XmlNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', XmlNode), 'The payload must contain a guideline profile.'); + Assert.AreEqual('urn.cpro.gouv.fr:1p0:CDV:einvoicingF2', XmlNode.AsXmlElement().InnerText(), 'The payload must use the PPF invoice profile.'); + AssertTradeParty(XmlDoc, 'SenderTradeParty', 'TEST-PLATFORM', '0238', 'Test Platform', 'WK'); + AssertTradeParty(XmlDoc, 'IssuerTradeParty', '123456789', '0002', 'FR Test Issuer', 'SE'); + AssertTradeParty(XmlDoc, 'RecipientTradeParty', '9998', '0238', 'PPF', 'DFH'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReceiptDateTime"]', XmlNode), 'The payload must contain the platform receipt time.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="FormattedIssueDateTime"]/*[local-name()="DateTimeString"]', XmlNode), 'The payload must contain the invoice issue date.'); + Assert.AreEqual(Format(EDocument."Document Date", 0, ''), XmlNode.AsXmlElement().InnerText(), 'The invoice issue date is incorrect.'); + end; + + local procedure AssertPayloadCDVContext(Payload: Text) + var + XmlDoc: XmlDocument; + XmlNode: XmlNode; + begin + Assert.IsTrue(XmlDocument.ReadFrom(Payload, XmlDoc), 'The payload must be valid XML.'); + Assert.IsTrue(XmlDoc.SelectSingleNode('//*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', XmlNode), 'The payload must contain a guideline profile.'); + Assert.AreEqual('urn.cpro.gouv.fr:1p0:CDV:invoice', XmlNode.AsXmlElement().InnerText(), 'The payload must use the CDV invoice profile.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="SenderTradeParty"]', XmlNode), 'The CDV payload must not contain a sender platform party.'); + Assert.IsFalse(XmlDoc.SelectSingleNode('//*[local-name()="RecipientTradeParty"]', XmlNode), 'The CDV payload must not contain a PPF recipient party.'); + end; + + local procedure AssertTradeParty(XmlDoc: XmlDocument; ElementName: Text; ExpectedID: Text; ExpectedScheme: Text; ExpectedName: Text; ExpectedRole: Text) + var + SchemeNode: XmlNode; + XmlNode: XmlNode; + PartyPath: Text; + begin + PartyPath := StrSubstNo('//*[local-name()="ExchangedDocument"]/*[local-name()="%1"]', ElementName); + Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]', XmlNode), 'The payload must contain the expected trade-party ID.'); + Assert.AreEqual(ExpectedID, XmlNode.AsXmlElement().InnerText(), 'The trade-party ID is incorrect.'); + Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]/@schemeID', SchemeNode), 'The trade-party ID must contain a scheme.'); + Assert.AreEqual(ExpectedScheme, SchemeNode.AsXmlAttribute().Value(), 'The trade-party scheme is incorrect.'); + Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="Name"]', XmlNode), 'The payload must contain the expected trade-party name.'); + Assert.AreEqual(ExpectedName, XmlNode.AsXmlElement().InnerText(), 'The trade-party name is incorrect.'); + Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="RoleCode"]', XmlNode), 'The payload must contain the expected trade-party role.'); + Assert.AreEqual(ExpectedRole, XmlNode.AsXmlElement().InnerText(), 'The trade-party role is incorrect.'); + end; + local procedure Initialize() var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; @@ -1060,6 +1178,7 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage.DeleteAll(); MessageSenderMock.Reset(); EnsureService(); + EnsureCompanyInformation(); GeneralLedgerSetup.Get(); if not GeneralLedgerSetup."Unrealized VAT" then begin GeneralLedgerSetup."Unrealized VAT" := true; @@ -1067,6 +1186,16 @@ codeunit 148151 "FR E-Invoice Message Tests" end; end; + local procedure EnsureCompanyInformation() + var + CompanyInformation: Record "Company Information"; + begin + CompanyInformation.Get(); + CompanyInformation."Registration No." := '123456789'; + CompanyInformation.Name := 'FR Test Issuer'; + CompanyInformation.Modify(); + end; + local procedure EnsureService() var EDocumentService: Record "E-Document Service"; @@ -1148,6 +1277,8 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Document Date" := SalesInvoiceHeader."Document Date"; + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; @@ -1213,6 +1344,8 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Document Date" := SalesInvoiceHeader."Document Date"; + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; @@ -1292,6 +1425,8 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Document Date" := SalesInvoiceHeader."Document Date"; + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; @@ -1345,6 +1480,8 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Document Date" := SalesInvoiceHeader."Document Date"; + EDocument."Clearance Date" := CurrentDateTime(); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Service := 'FR-MESSAGE-MOCK'; From 80c8837620631892dc6f15771c0a509127e9f646 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 13:37:34 +0200 Subject: [PATCH 15/49] Added Validatior, asynh --- .../EReportingFRUser.PermissionSet.al | 1 + .../src/Core/FREInvoiceMessageAPI.Codeunit.al | 36 +++ .../Core/FREInvoiceMessageBuilder.Codeunit.al | 2 + .../FREInvoiceProfileValidator.Codeunit.al | 132 ++++++++ .../src/FREInvoiceMessageTests.Codeunit.al | 298 ++++++++++++++++++ .../EDocCoreObjects.PermissionSet.al | 3 +- .../IMessageResponseHandler.Interface.al | 27 ++ .../Interfaces/IMessageSender.Interface.al | 3 +- .../Integration/ServiceIntegration.Enum.al | 8 +- .../EDocumentBackgroundJobs.Codeunit.al | 5 + .../Message/EDocMessageContext.Codeunit.al | 2 +- .../Message/EDocMessageMgt.Codeunit.al | 66 +++- .../EDocMessageResponseJob.Codeunit.al | 46 +++ .../Message/EDocMessageStatus.Enum.al | 8 + .../EDocMsgTransportDefault.Codeunit.al | 14 +- .../Message/EDocumentMessageAPI.Codeunit.al | 11 + .../Message/EDocumentMessagesFactBox.Page.al | 4 +- .../Mock/EDocIntegrationMockV2.Codeunit.al | 14 +- .../src/Mock/EDocIntegrationMockV2.EnumExt.al | 2 +- .../EDocMessageMgtTests.Codeunit.al | 178 ++++++++++- 20 files changed, 843 insertions(+), 17 deletions(-) create mode 100644 src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al index f1704d10295..bb817cedc60 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al @@ -20,6 +20,7 @@ permissionset 10988 "E-Reporting FR User" tabledata "FR E-Invoice Message VAT" = R, codeunit "FR E-Invoice Message Mgt." = X, codeunit "FR E-Invoice Message Builder" = X, + codeunit "FR E-Invoice Profile Validator" = X, codeunit "FR E-Invoice Message API" = X, page "FR E-Invoice Refusal Dialog" = X, page "FR E-Invoice Messages" = X; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al index 7568dd60e15..a529ae840f0 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al @@ -49,6 +49,7 @@ codeunit 10987 "FR E-Invoice Message API" EDocument.TestField(Direction, EDocument.Direction::Outgoing); if EDocument."Document No." <> InvoiceID then Error(InvoiceMismatchErr, InvoiceID, EDocument."Document No."); + ValidateLifecycleTransition(EDocument."Entry No", MessageType); FREInvoiceMessage.Init(); FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; @@ -68,6 +69,39 @@ codeunit 10987 "FR E-Invoice Message API" exit(FREInvoiceMessage."Entry No."); end; + local procedure ValidateLifecycleTransition(EDocumentEntryNo: Integer; NewMessageType: Enum "FR E-Invoice Message Type") + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + PreviousMessageType: Enum "FR E-Invoice Message Type"; + HasPreviousMessage: Boolean; + begin + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocumentEntryNo); + FREInvoiceMessage.SetFilter(Type, '%1|%2|%3|%4', FREInvoiceMessage.Type::Submitted, FREInvoiceMessage.Type::Accepted, + FREInvoiceMessage.Type::Refused, FREInvoiceMessage.Type::"Technical Rejected"); + if FREInvoiceMessage.FindLast() then begin + HasPreviousMessage := true; + PreviousMessageType := FREInvoiceMessage.Type; + end; + + case NewMessageType of + NewMessageType::Submitted: + if HasPreviousMessage then + Error(InvalidLifecycleTransitionErr, Format(PreviousMessageType), Format(NewMessageType)); + NewMessageType::Accepted, + NewMessageType::Refused, + NewMessageType::"Technical Rejected": + if (not HasPreviousMessage) or (PreviousMessageType <> PreviousMessageType::Submitted) then + Error(InvalidLifecycleTransitionErr, GetPreviousMessageTypeText(HasPreviousMessage, PreviousMessageType), Format(NewMessageType)); + end; + end; + + local procedure GetPreviousMessageTypeText(HasPreviousMessage: Boolean; PreviousMessageType: Enum "FR E-Invoice Message Type"): Text + begin + if HasPreviousMessage then + exit(Format(PreviousMessageType)); + exit(NoPreviousStatusTok); + end; + local procedure ParseMessage(TempBlob: Codeunit "Temp Blob"; var InvoiceID: Text; var MessageType: Enum "FR E-Invoice Message Type"; var ReasonCode: Text; var ReasonDescription: Text) var XmlDoc: XmlDocument; @@ -149,6 +183,8 @@ codeunit 10987 "FR E-Invoice Message API" StatusErr: Label 'The French invoice lifecycle message does not contain a status.'; UnsupportedStatusErr: Label 'French invoice lifecycle status %1 is not supported.', Comment = '%1 = lifecycle status'; InvoiceMismatchErr: Label 'The lifecycle message invoice ID %1 does not match E-Document invoice %2.', Comment = '%1 = message invoice identifier, %2 = E-Document invoice identifier'; + InvalidLifecycleTransitionErr: Label 'French invoice lifecycle status cannot change from %1 to %2.', Comment = '%1 = previous lifecycle status, %2 = new lifecycle status'; + NoPreviousStatusTok: Label 'no previous status'; RejectedReasonCodeErr: Label 'A technical rejection reason code is required.'; RejectedReasonDescriptionErr: Label 'A technical rejection reason description is required.'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index c3af9705b39..b74a72f41d9 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -16,6 +16,7 @@ codeunit 10976 "FR E-Invoice Message Builder" procedure BuildMessage(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; var TempBlob: Codeunit "Temp Blob") var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; XmlDoc: XmlDocument; RootElement: XmlElement; AcknowledgementElement: XmlElement; @@ -85,6 +86,7 @@ codeunit 10976 "FR E-Invoice Message Builder" AcknowledgementElement.Add(ReferenceElement); RootElement.Add(AcknowledgementElement); XmlDoc.Add(RootElement); + FREInvoiceProfileValidator.Validate(XmlDoc, IsPPFMessage(FREInvoiceMessage)); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); XmlDoc.WriteTo(OutStream); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al new file mode 100644 index 00000000000..a6e926ba21d --- /dev/null +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al @@ -0,0 +1,132 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Formats; + +codeunit 10988 "FR E-Invoice Profile Validator" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure Validate(XmlDoc: XmlDocument; IsPPFProfile: Boolean) + var + ExpectedProfileID: Text; + begin + RequireNode(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse" and namespace-uri()="' + RsmNamespaceTok + '"]'); + if IsPPFProfile then + ExpectedProfileID := PPFInvoiceProfileTok + else + ExpectedProfileID := CDVInvoiceProfileTok; + + RequireNodeText(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="ExchangedDocumentContext"]/*[local-name()="GuidelineSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', ExpectedProfileID); + RequireNode(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="ExchangedDocument"]/*[local-name()="ID"]'); + RequireDateTimeNode(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="ExchangedDocument"]/*[local-name()="IssueDateTime"]/*[local-name()="DateTimeString"]', DateTimeFormatCodeTok); + RequireNodeText(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="AcknowledgementDocument"]/*[local-name()="TypeCode"]', InformationTypeCodeTok); + RequireDateTimeNode(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="AcknowledgementDocument"]/*[local-name()="IssueDateTime"]/*[local-name()="DateTimeString"]', DateTimeFormatCodeTok); + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="StatusCode"]', InvoiceReferenceStatusCodeTok); + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="TypeCode"]', InvoiceTypeCodeTok); + RequireNode(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessConditionCode"]'); + + if IsPPFProfile then + ValidatePPFProfile(XmlDoc) + else + ValidateCDVProfile(XmlDoc); + end; + + local procedure ValidatePPFProfile(XmlDoc: XmlDocument) + begin + RequireTradeParty(XmlDoc, 'SenderTradeParty', '', '', SenderRoleCodeTok); + RequireTradeParty(XmlDoc, 'IssuerTradeParty', SIRENSchemeTok, '', SellerRoleCodeTok); + RequireTradeParty(XmlDoc, 'RecipientTradeParty', PPFIdentifierSchemeTok, PPFIdentifierTok, PPFRoleCodeTok); + RequireDateTimeNode(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReceiptDateTime"]/*[local-name()="DateTimeString"]', DateTimeFormatCodeTok); + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ReferenceTypeCode"]', PPFInvoiceProfileTok); + RequireDateTimeNode(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="FormattedIssueDateTime"]/*[local-name()="DateTimeString"]', DateFormatCodeTok); + end; + + local procedure ValidateCDVProfile(XmlDoc: XmlDocument) + var + XmlNode: XmlNode; + begin + RequireNodeText(XmlDoc, '//*[local-name()="BusinessProcessSpecifiedDocumentContextParameter"]/*[local-name()="ID"]', RegulatedBusinessProcessTok); + if XmlDoc.SelectSingleNode('//*[local-name()="SenderTradeParty" or local-name()="RecipientTradeParty"]', XmlNode) then + Error(UnexpectedProfileNodeErr, CDVInvoiceProfileTok); + end; + + local procedure RequireTradeParty(XmlDoc: XmlDocument; PartyName: Text; ExpectedSchemeID: Text; ExpectedID: Text; ExpectedRoleCode: Text) + var + GlobalIDNode: XmlNode; + SchemeNode: XmlNode; + RoleNode: XmlNode; + PartyPath: Text; + begin + PartyPath := '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="ExchangedDocument"]/*[local-name()="' + PartyName + '"]'; + if not XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]', GlobalIDNode) then + Error(RequiredProfileNodeErr, PartyPath + '/*[local-name()="GlobalID"]'); + if GlobalIDNode.AsXmlElement().InnerText() = '' then + Error(EmptyProfileNodeErr, PartyPath + '/*[local-name()="GlobalID"]'); + if (ExpectedID <> '') and (GlobalIDNode.AsXmlElement().InnerText() <> ExpectedID) then + Error(ProfileValueErr, PartyPath + '/*[local-name()="GlobalID"]', ExpectedID, GlobalIDNode.AsXmlElement().InnerText()); + if not XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]/@schemeID', SchemeNode) then + Error(RequiredProfileNodeErr, PartyPath + '/*[local-name()="GlobalID"]/@schemeID'); + if (ExpectedSchemeID <> '') and (SchemeNode.AsXmlAttribute().Value() <> ExpectedSchemeID) then + Error(ProfileValueErr, PartyPath + '/*[local-name()="GlobalID"]/@schemeID', ExpectedSchemeID, SchemeNode.AsXmlAttribute().Value()); + if not XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="RoleCode"]', RoleNode) then + Error(RequiredProfileNodeErr, PartyPath + '/*[local-name()="RoleCode"]'); + if RoleNode.AsXmlElement().InnerText() <> ExpectedRoleCode then + Error(ProfileValueErr, PartyPath + '/*[local-name()="RoleCode"]', ExpectedRoleCode, RoleNode.AsXmlElement().InnerText()); + end; + + local procedure RequireDateTimeNode(XmlDoc: XmlDocument; XPath: Text; ExpectedFormat: Text) + var + FormatNode: XmlNode; + begin + RequireNode(XmlDoc, XPath); + if not XmlDoc.SelectSingleNode(XPath + '/@format', FormatNode) then + Error(RequiredProfileNodeErr, XPath + '/@format'); + if FormatNode.AsXmlAttribute().Value() <> ExpectedFormat then + Error(ProfileValueErr, XPath + '/@format', ExpectedFormat, FormatNode.AsXmlAttribute().Value()); + end; + + local procedure RequireNode(XmlDoc: XmlDocument; XPath: Text) + var + XmlNode: XmlNode; + begin + if not XmlDoc.SelectSingleNode(XPath, XmlNode) then + Error(RequiredProfileNodeErr, XPath); + if XmlNode.IsXmlElement() and (XmlNode.AsXmlElement().InnerText() = '') then + Error(EmptyProfileNodeErr, XPath); + end; + + local procedure RequireNodeText(XmlDoc: XmlDocument; XPath: Text; ExpectedValue: Text) + var + XmlNode: XmlNode; + begin + if not XmlDoc.SelectSingleNode(XPath, XmlNode) then + Error(RequiredProfileNodeErr, XPath); + if XmlNode.AsXmlElement().InnerText() <> ExpectedValue then + Error(ProfileValueErr, XPath, ExpectedValue, XmlNode.AsXmlElement().InnerText()); + end; + + var + RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; + RegulatedBusinessProcessTok: Label 'REGULATED', Locked = true; + CDVInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:invoice', Locked = true; + PPFInvoiceProfileTok: Label 'urn.cpro.gouv.fr:1p0:CDV:einvoicingF2', Locked = true; + InformationTypeCodeTok: Label '23', Locked = true; + DateTimeFormatCodeTok: Label '204', Locked = true; + DateFormatCodeTok: Label '102', Locked = true; + InvoiceReferenceStatusCodeTok: Label '47', Locked = true; + InvoiceTypeCodeTok: Label '380', Locked = true; + SenderRoleCodeTok: Label 'WK', Locked = true; + SellerRoleCodeTok: Label 'SE', Locked = true; + SIRENSchemeTok: Label '0002', Locked = true; + PPFIdentifierTok: Label '9998', Locked = true; + PPFIdentifierSchemeTok: Label '0238', Locked = true; + PPFRoleCodeTok: Label 'DFH', Locked = true; + RequiredProfileNodeErr: Label 'The French invoice lifecycle payload does not contain required profile node %1.', Comment = '%1 = XPath of the required XML node'; + EmptyProfileNodeErr: Label 'The French invoice lifecycle payload contains an empty profile node %1.', Comment = '%1 = XPath of the empty XML node'; + ProfileValueErr: Label 'French invoice lifecycle profile node %1 must have value %2 instead of %3.', Comment = '%1 = XPath, %2 = expected value, %3 = actual value'; + UnexpectedProfileNodeErr: Label 'The French invoice lifecycle payload contains a trade-party node that is not allowed by profile %1.', Comment = '%1 = profile identifier'; +} \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index aaf8103bb05..12a79a125e0 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -791,6 +791,7 @@ codeunit 148151 "FR E-Invoice Message Tests" ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", '205', '', '')); @@ -819,6 +820,7 @@ codeunit 148151 "FR E-Invoice Message Tests" ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejetée', 'SCHEMA', 'Schema validation failed')); @@ -984,6 +986,251 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.ExpectedErrorCode('Dialog'); end; + [Test] + procedure ReceiveSubmittedThenRefusedIsValid() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + ExternalDocID: Text[250]; + FREntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] A Refused status follows a Submitted status. + Initialize(); + + // [GIVEN] Outgoing E-Document "ED" with a received Submitted status + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); + + // [WHEN] A Refused status is received + FREntryNo := ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Refused', '', ''); + + // [THEN] The Refused status is persisted + FREInvoiceMessage.Get(FREntryNo); + Assert.AreEqual(FREInvoiceMessage.Type::Refused, FREInvoiceMessage.Type, 'FR type must be Refused.'); + end; + + [Test] + procedure ReceiveSubmittedThenTechnicalRejectedIsValid() + var + EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + ExternalDocID: Text[250]; + FREntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] A Technical Rejected status follows a Submitted status. + Initialize(); + + // [GIVEN] Outgoing E-Document "ED" with a received Submitted status + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); + + // [WHEN] A Technical Rejected status is received + FREntryNo := ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Rejected', 'SCHEMA', 'Schema validation failed'); + + // [THEN] The Technical Rejected status is persisted + FREInvoiceMessage.Get(FREntryNo); + Assert.AreEqual(FREInvoiceMessage.Type::"Technical Rejected", FREInvoiceMessage.Type, 'FR type must be Technical Rejected.'); + end; + + [Test] + procedure ReceiveResponseBeforeSubmittedIsRejected() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + ExternalDocID: Text[250]; + begin + // [FEATURE] [AI test] + // [SCENARIO] A terminal response cannot be the first lifecycle status. + Initialize(); + + // [GIVEN] Outgoing E-Document "ED" without a lifecycle status + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + + // [WHEN] An Accepted status is received + asserterror ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Accepted', '', ''); + + // [THEN] The transition is rejected + Assert.ExpectedError('cannot change from no previous status to Accepted'); + end; + + [Test] + procedure ReceiveDuplicateSubmittedIsRejected() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + ExternalDocID: Text[250]; + begin + // [FEATURE] [AI test] + // [SCENARIO] Submitted cannot be received twice with different external message IDs. + Initialize(); + + // [GIVEN] Outgoing E-Document "ED" with a received Submitted status + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); + + // [WHEN] Another Submitted status is received + asserterror ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); + + // [THEN] The duplicate status is rejected + Assert.ExpectedError('cannot change from Submitted to Submitted'); + end; + + [Test] + procedure ReceiveStatusAfterTerminalStatusIsRejected() + var + EDocument: Record "E-Document"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + ExternalDocID: Text[250]; + begin + // [FEATURE] [AI test] + // [SCENARIO] No status can follow a terminal lifecycle response. + Initialize(); + + // [GIVEN] Outgoing E-Document "ED" with Submitted and Refused statuses + CreateOutgoingEDocument(EDocument); + ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); + ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Refused', '', ''); + + // [WHEN] An Accepted status is received + asserterror ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Accepted', '', ''); + + // [THEN] The transition from the terminal status is rejected + Assert.ExpectedError('cannot change from Refused to Accepted'); + end; + + [Test] + procedure CompletePPFProfileIsValid() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] A complete PPF lifecycle payload satisfies profile validation. + Initialize(); + + // [GIVEN] A complete PPF lifecycle document + XmlDocument.ReadFrom(BuildPPFValidationXml(PPFProfileID(), true, 'WK', '0238', '102'), XmlDoc); + + // [WHEN] The PPF profile is validated + FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] No validation error occurs + end; + + [Test] + procedure PPFProfileRejectsWrongProfileID() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] A PPF lifecycle payload must declare the PPF profile. + Initialize(); + + // [GIVEN] A PPF lifecycle document with the CDV profile ID + XmlDocument.ReadFrom(BuildPPFValidationXml(CDVProfileID(), true, 'WK', '0238', '102'), XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The incorrect profile ID is rejected + Assert.ExpectedError('must have value ' + PPFProfileID()); + end; + + [Test] + procedure PPFProfileRequiresSenderTradeParty() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] A PPF lifecycle payload requires the sender platform party. + Initialize(); + + // [GIVEN] A PPF lifecycle document without a sender platform party + XmlDocument.ReadFrom(BuildPPFValidationXml(PPFProfileID(), false, 'WK', '0238', '102'), XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The missing sender platform is rejected + Assert.ExpectedError('SenderTradeParty'); + end; + + [Test] + procedure PPFProfileRejectsWrongRecipientScheme() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] The PPF recipient must use scheme 0238. + Initialize(); + + // [GIVEN] A PPF lifecycle document with the wrong recipient scheme + XmlDocument.ReadFrom(BuildPPFValidationXml(PPFProfileID(), true, 'WK', '9999', '102'), XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The incorrect recipient scheme is rejected + Assert.ExpectedError('must have value 0238 instead of 9999'); + end; + + [Test] + procedure PPFProfileRejectsWrongSenderRole() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] The sender platform must use role WK. + Initialize(); + + // [GIVEN] A PPF lifecycle document with the wrong sender role + XmlDocument.ReadFrom(BuildPPFValidationXml(PPFProfileID(), true, 'XX', '0238', '102'), XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The incorrect sender role is rejected + Assert.ExpectedError('must have value WK instead of XX'); + end; + + [Test] + procedure PPFProfileRejectsWrongInvoiceDateFormat() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + begin + // [FEATURE] [AI test] + // [SCENARIO] The PPF invoice issue date must use format 102. + Initialize(); + + // [GIVEN] A PPF lifecycle document with the wrong invoice date format + XmlDocument.ReadFrom(BuildPPFValidationXml(PPFProfileID(), true, 'WK', '0238', '204'), XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The incorrect date format is rejected + Assert.ExpectedError('must have value 102 instead of 204'); + end; + [Test] procedure BuilderRejectsIncomingOnlyStatus() var @@ -1033,6 +1280,57 @@ codeunit 148151 "FR E-Invoice Message Tests" exit(XmlText.ToText()); end; + local procedure ReceiveLifecycleMessage(EDocument: Record "E-Document"; ExternalDocID: Text[250]; Status: Text; ReasonCode: Text; ReasonDescription: Text): Integer + var + FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + begin + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", Status, ReasonCode, ReasonDescription)); + exit(FREInvoiceMessageAPI.ReceiveMessage( + EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob)); + end; + + local procedure BuildPPFValidationXml(ProfileID: Text; IncludeSender: Boolean; SenderRole: Text; RecipientScheme: Text; InvoiceDateFormat: Text): Text + var + XmlText: TextBuilder; + begin + XmlText.Append(''); + XmlText.Append(''); + XmlText.Append(ProfileID); + XmlText.Append(''); + XmlText.Append('MESSAGE-ID20260821120000'); + if IncludeSender then begin + XmlText.Append('SENDER'); + XmlText.Append(SenderRole); + XmlText.Append(''); + end; + XmlText.Append('123456789SE'); + XmlText.Append('9998DFH'); + XmlText.Append('2320260821000000'); + XmlText.Append('INVOICE47380'); + XmlText.Append('20260821120000'); + XmlText.Append(PPFProfileID()); + XmlText.Append('20260821205'); + XmlText.Append(''); + exit(XmlText.ToText()); + end; + + local procedure PPFProfileID(): Text + begin + exit('urn.cpro.gouv.fr:1p0:CDV:einvoicingF2'); + end; + + local procedure CDVProfileID(): Text + begin + exit('urn.cpro.gouv.fr:1p0:CDV:invoice'); + end; + local procedure SendFirstMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type") var FREInvoiceMessage: Record "FR E-Invoice Message"; diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index b5c4b2029d1..1d09834768a 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -182,5 +182,6 @@ permissionset 6100 "E-Doc. Core - Objects" codeunit "Sent Document Approval" = X, codeunit "Sent Document Cancellation" = X, codeunit "E-Doc. Remittance Advice Mgt." = X, - codeunit "E-Doc. Remit. Advice Export" = X; + codeunit "E-Doc. Remit. Advice Export" = X, + codeunit "E-Doc. Message Response Job" = X; } diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al new file mode 100644 index 00000000000..12d81d7813c --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Integration.Interfaces; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; + +/// +/// Polls the asynchronous response for an outgoing child E-Document message. +/// +interface IMessageResponseHandler +{ + /// + /// Polls the external service for the response to a previously sent child message. + /// + /// The parent E-Document. + /// The service used to send the message. + /// The original message context and transport diagnostics. + /// True when the response is complete; otherwise false. + /// + /// A completed response must set the context status to Sent. A response that is not ready must set it to Pending Response. + /// Implementations must use MessageContext.GetMessageEntryNo() to correlate the external request. + /// + procedure GetResponse(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"): Boolean; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al index 3204c010ba4..698db3b63da 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.Interface.al @@ -17,10 +17,11 @@ interface IMessageSender /// /// The parent E-Document. /// The service used to send the message. - /// The message payload, transport state, and result. The implementation must set the result to Sent after successful transmission. + /// The message payload, transport state, and result. The implementation must set the result to Sent or Pending Response after successful transmission. /// /// The implementation is responsible for obtaining any privacy consent required by the external service before transmitting data. /// Implementations must use MessageContext.GetMessageEntryNo() as an idempotency key because a message can be retried after an ambiguous transport failure. + /// Set the context status to Pending Response when the external service accepted the message but requires asynchronous response polling. /// procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"); } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al index f84d85f9812..041f0603521 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al @@ -8,13 +8,15 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration.Interfaces; using Microsoft.eServices.EDocument.Processing.Message; -enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, IConsentManager, IMessageSender +enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, IConsentManager, IMessageSender, IMessageResponseHandler { Extensible = true; Access = Public; DefaultImplementation = IConsentManager = "Consent Manager Default Impl.", - IMessageSender = "E-Doc. Msg. Transport Default"; - UnknownValueImplementation = IMessageSender = "E-Doc. Msg. Transport Default"; + IMessageSender = "E-Doc. Msg. Transport Default", + IMessageResponseHandler = "E-Doc. Msg. Transport Default"; + UnknownValueImplementation = IMessageSender = "E-Doc. Msg. Transport Default", + IMessageResponseHandler = "E-Doc. Msg. Transport Default"; value(0; "No Integration") { diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 0ffe24b98c7..ca180305fb7 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -27,6 +27,11 @@ codeunit 6133 "E-Document Background Jobs" ScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0); end; + procedure ScheduleMessageResponse(EDocumentMessage: Record "E-Document Message") + begin + ScheduleEDocumentJob(Codeunit::"E-Doc. Message Response Job", EDocumentMessage.RecordId(), 300000); + end; + procedure ScheduleGetResponseJob() begin ScheduleGetResponseJob(true); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al index a6f93a5f472..44443b859e0 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al @@ -68,7 +68,7 @@ codeunit 6533 "E-Doc. Message Context" end; /// - /// Gets the transport result. A connector must set the status to Sent after successful transmission. + /// Gets the transport result. A connector must set the status to Sent or Pending Response after successful transmission. /// /// The integration action status. procedure Status(): Codeunit "Integration Action Status" diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 025c5892ddc..d845d47d6c0 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -118,6 +118,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentService: Record "E-Document Service"; EDocMessage: Record "E-Document Message"; EDocMessageContext: Codeunit "E-Doc. Message Context"; + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; @@ -138,7 +139,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); - if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then begin + if not (EDocMessageContext.Status().GetStatus() in ["E-Document Service Status"::Sent, "E-Document Service Status"::"Pending Response"]) then begin MessageSendingErrorInfo.ErrorType := ErrorType::Internal; MessageSendingErrorInfo.Message := StrSubstNo(MessageSendingErr, MessageEntryNo); MessageSendingErrorInfo.DetailedMessage := StrSubstNo(MessageSendingDetailedErr, EDocMessageContext.Status().GetStatus()); @@ -147,10 +148,57 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); - EDocMessage.Status := EDocMessage.Status::Sent; + if EDocMessageContext.Status().GetStatus() = "E-Document Service Status"::"Pending Response" then + EDocMessage.Status := EDocMessage.Status::"Pending Response" + else + EDocMessage.Status := EDocMessage.Status::Sent; + EDocMessage."Last Attempt At" := CurrentDateTime(); + Clear(EDocMessage."Last Error"); + EDocMessage.Modify(); + if EDocMessage.Status = EDocMessage.Status::"Pending Response" then + EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + end; + + procedure PollMessageResponse(MessageEntryNo: Integer) + var + EDocument: Record "E-Document"; + EDocumentService: Record "E-Document Service"; + EDocMessage: Record "E-Document Message"; + EDocMessageContext: Codeunit "E-Doc. Message Context"; + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; + EDocumentLog: Codeunit "E-Document Log"; + TempBlob: Codeunit "Temp Blob"; + MessageResponseHandler: Interface IMessageResponseHandler; + ResponseReceived: Boolean; + begin + EDocMessage.Get(MessageEntryNo); + EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); + EDocMessage.TestField(Status, EDocMessage.Status::"Pending Response"); + EDocMessage.TestField(Service); + EDocument.Get(EDocMessage."E-Document Entry No."); + EDocumentService.Get(EDocMessage.Service); + GetMessageBlob(MessageEntryNo, TempBlob); + EDocMessageContext.Initialize(EDocMessage, TempBlob); + MessageResponseHandler := EDocumentService."Service Integration V2"; + ResponseReceived := MessageResponseHandler.GetResponse(EDocument, EDocumentService, EDocMessageContext); + + if ResponseReceived then begin + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then + Error(MessageResponseStatusErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + EDocMessage.Status := EDocMessage.Status::Sent; + end else begin + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::"Pending Response" then + Error(MessageResponseStatusErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + EDocMessage.Status := EDocMessage.Status::"Pending Response"; + end; + + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); EDocMessage."Last Attempt At" := CurrentDateTime(); Clear(EDocMessage."Last Error"); EDocMessage.Modify(); + if not ResponseReceived then + EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); end; procedure QueueMessage(MessageEntryNo: Integer) @@ -176,13 +224,20 @@ codeunit 6433 "E-Doc. Message Mgt." begin EDocMessage.Get(MessageEntryNo); EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); - EDocMessage.TestField(Status, EDocMessage.Status::Error); + if EDocMessage.Status <> EDocMessage.Status::"Response Error" then + EDocMessage.TestField(Status, EDocMessage.Status::Error); EDocMessage.TestField(Service); - EDocMessage.Status := EDocMessage.Status::Queued; + if EDocMessage.Status = EDocMessage.Status::"Response Error" then + EDocMessage.Status := EDocMessage.Status::"Pending Response" + else + EDocMessage.Status := EDocMessage.Status::Queued; EDocMessage.Modify(); Commit(); - EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); + if EDocMessage.Status = EDocMessage.Status::"Pending Response" then + EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage) + else + EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); end; procedure RegisterExternalDocumentReference(EDocument: Record "E-Document"; ServiceCode: Code[20]; ExternalDocumentID: Text[250]) @@ -273,6 +328,7 @@ codeunit 6433 "E-Doc. Message Mgt." MessagePayloadErr: Label 'E-Document message %1 does not contain a payload.', Comment = '%1 = E-Document message entry number'; MessageSendingErr: Label 'E-Document message %1 could not be sent.', Comment = '%1 = E-Document message entry number'; MessageSendingDetailedErr: Label 'The E-Document message integration returned status %1.', Comment = '%1 = integration status'; + MessageResponseStatusErr: Label 'The connector returned invalid response status %2 for E-Document message %1.', Comment = '%1 = message entry number, %2 = connector status'; ExternalDocumentIDRequiredErr: Label 'An external document ID is required.'; ExternalMessageIDRequiredErr: Label 'An external message ID is required.'; IncomingMessagePayloadRequiredErr: Label 'An incoming E-Document message payload is required.'; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al new file mode 100644 index 00000000000..e0f235d863f --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +using System.Threading; + +codeunit 6539 "E-Doc. Message Response Job" +{ + Access = Internal; + TableNo = "Job Queue Entry"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + EDocumentMessage: Record "E-Document Message"; + LastErrorText: Text; + begin + EDocumentMessage.Get(Rec."Record ID to Process"); + if TryPollMessageResponse(EDocumentMessage."Entry No.") then + exit; + + LastErrorText := GetLastErrorText(); + EDocumentMessage.Get(EDocumentMessage."Entry No."); + EDocumentMessage.Status := EDocumentMessage.Status::"Response Error"; + EDocumentMessage."Last Attempt At" := CurrentDateTime(); + EDocumentMessage."Retry Count" += 1; + EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); + EDocumentMessage.Modify(); + Commit(); + Error(MessageResponseFailedErr, EDocumentMessage."Entry No.", LastErrorText); + end; + + [TryFunction] + local procedure TryPollMessageResponse(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.PollMessageResponse(MessageEntryNo); + end; + + var + MessageResponseFailedErr: Label 'The response for E-Document message %1 could not be retrieved. %2', Comment = '%1 = message entry number, %2 = connector error'; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al index fdca39dd3b8..c01e0b5c825 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al @@ -31,4 +31,12 @@ enum 6429 "E-Doc. Message Status" { Caption = 'Received'; } + value(5; "Pending Response") + { + Caption = 'Pending response'; + } + value(6; "Response Error") + { + Caption = 'Response error'; + } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al index 1a407bc3c75..a8f18bbfa9e 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al @@ -7,7 +7,7 @@ namespace Microsoft.eServices.EDocument.Processing.Message; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration.Interfaces; -codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender +codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender, IMessageResponseHandler { Access = Internal; InherentEntitlements = X; @@ -24,7 +24,19 @@ codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender Error(MessageTransportErrorInfo); end; + procedure GetResponse(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"): Boolean + var + MessageTransportErrorInfo: ErrorInfo; + begin + MessageTransportErrorInfo.Message := StrSubstNo(MessageResponseNotSupportedErr, EDocumentService.Code); + MessageTransportErrorInfo.RecordId := EDocumentService.RecordId; + MessageTransportErrorInfo.PageNo := Page::"E-Document Service"; + MessageTransportErrorInfo.AddNavigationAction(ShowEDocumentServiceLbl); + Error(MessageTransportErrorInfo); + end; + var MessageTransportNotSupportedErr: Label 'E-Document service %1 does not support sending E-Document messages.', Comment = '%1 = E-Document service code'; + MessageResponseNotSupportedErr: Label 'E-Document service %1 does not support polling E-Document message responses.', Comment = '%1 = E-Document service code'; ShowEDocumentServiceLbl: Label 'Open E-Document Service'; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al index 969c10e296c..38bf5d304d6 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al @@ -108,6 +108,17 @@ codeunit 6532 "E-Document Message API" EDocMessageMgt.RetryMessage(MessageEntryNo); end; + /// + /// Polls the service for the asynchronous response to an outgoing child message. + /// + /// The entry number of a message in Pending Response status. + procedure PollMessageResponse(MessageEntryNo: Integer) + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.PollMessageResponse(MessageEntryNo); + end; + /// /// Associates an external service document identifier with an E-Document for later message correlation. /// diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al index 8de33297620..5bfdf683f25 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al @@ -90,7 +90,7 @@ page 6434 "E-Document Messages FactBox" { ApplicationArea = Basic, Suite; Caption = 'Retry'; - ToolTip = 'Requeue the failed message for background transmission using its existing payload.'; + ToolTip = 'Retry the failed message transmission or response polling operation using its existing payload.'; Image = Refresh; Scope = Repeater; Enabled = RetryEnabled; @@ -131,7 +131,7 @@ page 6434 "E-Document Messages FactBox" trigger OnAfterGetCurrRecord() begin - RetryEnabled := (Rec.Direction = Rec.Direction::Outgoing) and (Rec.Status = Rec.Status::Error); + RetryEnabled := (Rec.Direction = Rec.Direction::Outgoing) and (Rec.Status in [Rec.Status::Error, Rec.Status::"Response Error"]); end; var diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al index 8042be9ea28..63de072b247 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al @@ -10,7 +10,7 @@ using Microsoft.eServices.EDocument.Integration.Receive; using Microsoft.eServices.EDocument.Integration.Send; using System.Utilities; -codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager +codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager, IMessageResponseHandler { Access = Internal; @@ -37,6 +37,18 @@ codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocume exit(Success); end; + procedure GetResponse(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"): Boolean + var + ResponseReceived: Boolean; + begin + OnGetResponse(EDocument, MessageContext.Http().GetHttpRequestMessage(), MessageContext.Http().GetHttpResponseMessage(), ResponseReceived); + if ResponseReceived then + MessageContext.Status().SetStatus("E-Document Service Status"::Sent) + else + MessageContext.Status().SetStatus("E-Document Service Status"::"Pending Response"); + exit(ResponseReceived); + end; + procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) begin OnReceiveDocuments(DocumentsMetadata, ReceiveContext.Http().GetHttpRequestMessage(), ReceiveContext.Http().GetHttpResponseMessage()); diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al index 1439adf28d6..d68f40bc8cd 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al @@ -12,7 +12,7 @@ enumextension 139617 "E-Doc Integration Mock V2" extends "Service Integration" value(133501; "Mock") { - Implementation = IDocumentSender = "E-Doc. Integration Mock V2", IDocumentReceiver = "E-Doc. Integration Mock V2", IConsentManager = "E-Doc. Integration Mock V2"; + Implementation = IDocumentSender = "E-Doc. Integration Mock V2", IDocumentReceiver = "E-Doc. Integration Mock V2", IConsentManager = "E-Doc. Integration Mock V2", IMessageResponseHandler = "E-Doc. Integration Mock V2"; } value(133502; "Mock Sync") { diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index dbea8a46ad7..8dfcc12042e 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -20,6 +20,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" var EDocumentService: Record "E-Document Service"; Assert: Codeunit Assert; + EDocImplState: Codeunit "E-Doc. Impl. State"; LibraryEDoc: Codeunit "Library - E-Document"; LibraryLowerPermission: Codeunit "Library - Lower Permissions"; IsInitialized: Boolean; @@ -169,6 +170,162 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" Assert.ExpectedError('Direction must have the value Outgoing'); end; + [Test] + procedure PollMessageResponseCompletesPendingMessage() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] A completed asynchronous response marks the existing child message as Sent. + Initialize(Customer); + + // [GIVEN] An outgoing child message waiting for a connector response + CreateOutgoingEDocument(EDocument); + MessageEntryNo := CreatePendingMessage(EDocument); + BindSubscription(EDocImplState); + EDocImplState.SetOnGetResponseSuccess(); + + // [WHEN] The connector reports that the response is complete + EDocumentMessageAPI.PollMessageResponse(MessageEntryNo); + UnbindSubscription(EDocImplState); + + // [THEN] The existing child message is marked Sent + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::Sent, EDocMessage.Status, 'The completed message must be Sent.'); + Assert.AreNotEqual(0DT, EDocMessage."Last Attempt At", 'The polling attempt time must be stored.'); + end; + + [Test] + procedure PollMessageResponseReschedulesPendingMessage() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] An incomplete asynchronous response remains pending and schedules another poll. + Initialize(Customer); + + // [GIVEN] An outgoing child message waiting for a connector response + CreateOutgoingEDocument(EDocument); + MessageEntryNo := CreatePendingMessage(EDocument); + BindSubscription(EDocImplState); + + // [WHEN] The connector reports that the response is still pending + EDocumentMessageAPI.PollMessageResponse(MessageEntryNo); + UnbindSubscription(EDocImplState); + + // [THEN] The message remains pending and one response job is scheduled + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::"Pending Response", EDocMessage.Status, 'The message must remain pending.'); + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Response Job"); + JobQueueEntry.SetRange("Record ID to Process", EDocMessage.RecordId()); + Assert.RecordCount(JobQueueEntry, 1); + end; + + [Test] + procedure PollMessageResponseJobStoresConnectorError() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] A connector polling failure is persisted on the existing child message. + Initialize(Customer); + + // [GIVEN] A pending child message and a connector that raises a runtime error + CreateOutgoingEDocument(EDocument); + MessageEntryNo := CreatePendingMessage(EDocument); + EDocMessage.Get(MessageEntryNo); + JobQueueEntry."Record ID to Process" := EDocMessage.RecordId(); + BindSubscription(EDocImplState); + EDocImplState.SetThrowIntegrationRuntimeError(); + + // [WHEN] The response polling background job runs + Assert.IsFalse(Codeunit.Run(Codeunit::"E-Doc. Message Response Job", JobQueueEntry), 'The polling job must report the connector failure.'); + UnbindSubscription(EDocImplState); + + // [THEN] The existing message contains response-error diagnostics + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::"Response Error", EDocMessage.Status, 'The message must have a response error.'); + Assert.AreEqual(1, EDocMessage."Retry Count", 'The failed polling attempt must increment the retry count.'); + Assert.IsTrue(EDocMessage."Last Error".Contains('TEST'), 'The connector error must be stored.'); + end; + + [Test] + procedure RetryMessageReschedulesFailedResponsePoll() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] Retrying a response error schedules polling instead of resending the child message. + Initialize(Customer); + + // [GIVEN] An outgoing child message whose response polling failed + CreateOutgoingEDocument(EDocument); + MessageEntryNo := CreatePendingMessage(EDocument); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::"Response Error"; + EDocMessage.Modify(); + + // [WHEN] The failed message is retried + EDocumentMessageAPI.RetryMessage(MessageEntryNo); + + // [THEN] The message is pending and only a response polling job is scheduled + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::"Pending Response", EDocMessage.Status, 'Retry must restore Pending Response status.'); + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Response Job"); + JobQueueEntry.SetRange("Record ID to Process", EDocMessage.RecordId()); + Assert.RecordCount(JobQueueEntry, 1); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Send Job"); + Assert.RecordCount(JobQueueEntry, 0); + end; + + [Test] + procedure PollMessageResponseRejectsUnsupportedConnector() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO] A service without child response support returns an actionable error. + Initialize(Customer); + + // [GIVEN] A pending child message for a service without a response handler + EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"No Integration"; + EDocumentService.Modify(); + CreateOutgoingEDocument(EDocument); + MessageEntryNo := CreatePendingMessage(EDocument); + + // [WHEN] The message response is polled + asserterror EDocumentMessageAPI.PollMessageResponse(MessageEntryNo); + + // [THEN] The unsupported connector is reported + Assert.ExpectedError('does not support polling E-Document message responses'); + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::"Pending Response", EDocMessage.Status, 'Direct polling failure must not discard the pending state.'); + end; + local procedure Initialize(var Customer: Record Customer) var EDocument: Record "E-Document"; @@ -177,7 +334,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" begin LibraryLowerPermission.SetOutsideO365Scope(); JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); - JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Message Send Job"); + JobQueueEntry.SetFilter("Object ID to Run", '%1|%2', Codeunit::"E-Doc. Message Send Job", Codeunit::"E-Doc. Message Response Job"); JobQueueEntry.DeleteAll(); EDocMessage.DeleteAll(); EDocument.DeleteAll(); @@ -199,4 +356,23 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" EDocument.Service := EDocumentService.Code; EDocument.Insert(); end; + + local procedure CreatePendingMessage(EDocument: Record "E-Document"): Integer + var + EDocMessage: Record "E-Document Message"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::"Pending Response"; + EDocMessage.Modify(); + exit(MessageEntryNo); + end; } From 67d718e26d59ec8cec68614d92e6a91939b39aa3 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 14:12:39 +0200 Subject: [PATCH 16/49] errors compile --- .../Test/src/Mock/EDocIntegrationMockV2.Codeunit.al | 1 + .../src/Processing/EDocMessageMgtTests.Codeunit.al | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al index 63de072b247..0b1706f65bf 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration.Interfaces; using Microsoft.eServices.EDocument.Integration.Receive; using Microsoft.eServices.EDocument.Integration.Send; +using Microsoft.eServices.EDocument.Processing.Message; using System.Utilities; codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager, IMessageResponseHandler diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 8dfcc12042e..773f1ed74fd 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -46,7 +46,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::"Unspecified", "E-Document Direction"::Outgoing, + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, "E-Doc. Response Type"::None, TempBlob); // [WHEN] The message is queued @@ -84,7 +84,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, "E-Doc. Response Type"::None, TempBlob); EDocMessage.Get(MessageEntryNo); EDocMessage.Status := EDocMessage.Status::Error; @@ -126,7 +126,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, "E-Doc. Response Type"::None, TempBlob); // [WHEN] The message is retried @@ -157,7 +157,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Incoming, + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Incoming, "E-Doc. Response Type"::None, TempBlob); EDocMessage.Get(MessageEntryNo); EDocMessage.Status := EDocMessage.Status::Error; @@ -368,7 +368,7 @@ codeunit 139899 "E-Doc. Message Mgt. Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::Unspecified, "E-Document Direction"::Outgoing, + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, "E-Doc. Response Type"::None, TempBlob); EDocMessage.Get(MessageEntryNo); EDocMessage.Status := EDocMessage.Status::"Pending Response"; From 05f4c3440826ba5c12c01885230ef93c5c2c0e34 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 21 Aug 2026 19:26:18 +0200 Subject: [PATCH 17/49] tests fix --- .../app/src/Core/CIIXMLBuilder.Codeunit.al | 146 ++++++++++++++---- .../app/src/Core/EDocHelpers.Codeunit.al | 4 +- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 20 ++- .../src/FREInvoiceMessageTests.Codeunit.al | 65 ++++++-- .../test/src/FacturXCIIXMLTests.Codeunit.al | 108 +++++++++++-- 5 files changed, 281 insertions(+), 62 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al index ad44e43245d..64ec2c0482f 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al @@ -46,9 +46,10 @@ codeunit 10978 "CII XML Builder" RootElement := XmlElement.Create('CrossIndustryInvoice', RsmNamespaceTok); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('ram', RamNamespaceTok)); + RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('qdt', QdtNamespaceTok)); RootElement.Add(XmlAttribute.CreateNamespaceDeclaration('udt', UdtNamespaceTok)); - AddExchangedDocumentContext(RootElement); + AddExchangedDocumentContext(RootElement, SourceDocumentLines); AddExchangedDocument(RootElement, EDocument, TypeCode); AddSupplyChainTradeTransaction(RootElement, EDocument, SourceDocumentHeader, SourceDocumentLines, CompanyInformation); @@ -58,14 +59,20 @@ codeunit 10978 "CII XML Builder" XmlDoc.WriteTo(OutStr); end; - local procedure AddExchangedDocumentContext(var RootElement: XmlElement) + local procedure AddExchangedDocumentContext(var RootElement: XmlElement; var SourceDocumentLines: RecordRef) var ContextElement: XmlElement; + BusinessProcessElement: XmlElement; GuidelineElement: XmlElement; IdElement: XmlElement; begin ContextElement := XmlElement.Create('ExchangedDocumentContext', RsmNamespaceTok); + BusinessProcessElement := XmlElement.Create('BusinessProcessSpecifiedDocumentContextParameter', RamNamespaceTok); + IdElement := XmlElement.Create('ID', RamNamespaceTok, GetBillingMode(SourceDocumentLines)); + BusinessProcessElement.Add(IdElement); + ContextElement.Add(BusinessProcessElement); + GuidelineElement := XmlElement.Create('GuidelineSpecifiedDocumentContextParameter', RamNamespaceTok); IdElement := XmlElement.Create('ID', RamNamespaceTok, FacturXProfileIdTok); GuidelineElement.Add(IdElement); @@ -74,6 +81,34 @@ codeunit 10978 "CII XML Builder" RootElement.Add(ContextElement); end; + local procedure GetBillingMode(var SourceDocumentLines: RecordRef): Text + var + FREDocHelpers: Codeunit "EDoc. Helpers"; + TypeFieldRef: FieldRef; + HasItemLine: Boolean; + HasServiceLine: Boolean; + LineType: Text; + begin + if not FREDocHelpers.FindFieldByName(SourceDocumentLines, 'Type', TypeFieldRef) then + exit(ServiceBillingModeTok); + + if SourceDocumentLines.FindSet() then + repeat + LineType := DelChr(Format(TypeFieldRef.Value()), '=', ' '); + if LineType = ItemLineTypeTok then + HasItemLine := true + else + if LineType <> '' then + HasServiceLine := true; + until SourceDocumentLines.Next() = 0; + + if HasItemLine and HasServiceLine then + exit(MixedBillingModeTok); + if HasItemLine then + exit(GoodsBillingModeTok); + exit(ServiceBillingModeTok); + end; + local procedure AddExchangedDocument(var RootElement: XmlElement; var EDocument: Record "E-Document"; TypeCode: Text) var DocElement: XmlElement; @@ -141,6 +176,7 @@ codeunit 10978 "CII XML Builder" AddSellerTradeParty(AgreementElement, CompanyInformation); AddBuyerTradeParty(AgreementElement, SourceDocumentHeader); + AddInvoiceReferencedDocument(AgreementElement, SourceDocumentHeader); // BT-13 Purchase order reference if FREDocHelpers.FindFieldByName(SourceDocumentHeader, 'Order No.', FieldRefVar) then @@ -158,6 +194,35 @@ codeunit 10978 "CII XML Builder" TransactionElement.Add(AgreementElement); end; + local procedure AddInvoiceReferencedDocument(var AgreementElement: XmlElement; var SourceDocumentHeader: RecordRef) + var + SalesInvoiceHeader: Record "Sales Invoice Header"; + FREDocHelpers: Codeunit "EDoc. Helpers"; + AppliesToDocumentNoFieldRef: FieldRef; + DateStringElement: XmlElement; + FormattedIssueDateElement: XmlElement; + InvoiceReferenceElement: XmlElement; + AppliesToDocumentNo: Code[20]; + begin + if SourceDocumentHeader.Number() <> Database::"Sales Cr.Memo Header" then + exit; + if not FREDocHelpers.FindFieldByName(SourceDocumentHeader, 'Applies-to Doc. No.', AppliesToDocumentNoFieldRef) then + exit; + + AppliesToDocumentNo := AppliesToDocumentNoFieldRef.Value(); + if (AppliesToDocumentNo = '') or not SalesInvoiceHeader.Get(AppliesToDocumentNo) then + exit; + + InvoiceReferenceElement := XmlElement.Create('InvoiceReferencedDocument', RamNamespaceTok); + InvoiceReferenceElement.Add(XmlElement.Create('IssuerAssignedID', RamNamespaceTok, AppliesToDocumentNo)); + FormattedIssueDateElement := XmlElement.Create('FormattedIssueDateTime', RamNamespaceTok); + DateStringElement := XmlElement.Create('DateTimeString', QdtNamespaceTok, FormatDate(SalesInvoiceHeader."Document Date")); + DateStringElement.SetAttribute('format', '102'); + FormattedIssueDateElement.Add(DateStringElement); + InvoiceReferenceElement.Add(FormattedIssueDateElement); + AgreementElement.Add(InvoiceReferenceElement); + end; + local procedure AddSellerTradeParty(var AgreementElement: XmlElement; CompanyInformation: Record "Company Information") var SellerElement: XmlElement; @@ -213,6 +278,7 @@ codeunit 10978 "CII XML Builder" NameElement: XmlElement; CustomerNo: Code[20]; VATRegistrationNo: Text; + BuyerElectronicAddress: Text; BuyerElectronicAddressEmitted: Boolean; begin BuyerElement := XmlElement.Create('BuyerTradeParty', RamNamespaceTok); @@ -235,16 +301,12 @@ codeunit 10978 "CII XML Builder" // BT-49 Buyer electronic routing address is held only on the live customer master record. // BR-FR-12: BT-49 is mandatory in French e-invoicing. - Customer.SetLoadFields("FR Electronic Address", "FR Elec. Address Scheme", "Registration Number"); + Customer.SetLoadFields("FR Electronic Address", "Registration Number", "VAT Registration No."); if (CustomerNo <> '') and Customer.Get(CustomerNo) then - if Customer."FR Electronic Address" <> '' then begin - AddElectronicAddress(BuyerElement, Customer."FR Electronic Address", GetElecAddressSchemeCode(Customer."FR Elec. Address Scheme")); + if TryGetBuyerElectronicAddress(Customer, BuyerElectronicAddress) then begin + AddElectronicAddress(BuyerElement, BuyerElectronicAddress, '0225'); BuyerElectronicAddressEmitted := true; - end else - if Customer."Registration Number" <> '' then begin - AddElectronicAddress(BuyerElement, CopyStr(Customer."Registration Number", 1, 14), '0009'); - BuyerElectronicAddressEmitted := true; - end; + end; VATRegistrationNo := GetHeaderFieldText(SourceDocumentHeader, 'VAT Registration No.', ''); @@ -253,11 +315,50 @@ codeunit 10978 "CII XML Builder" AddElectronicAddress(BuyerElement, VATRegistrationNo, '9957'); if VATRegistrationNo <> '' then - AddVATRegistration(BuyerElement, VATRegistrationNo); + AddVATRegistration( + BuyerElement, + NormalizeBuyerVATRegistrationNo( + VATRegistrationNo, GetHeaderFieldText(SourceDocumentHeader, 'Sell-to Country/Region Code', 'Country/Region Code'))); AgreementElement.Add(BuyerElement); end; + local procedure NormalizeBuyerVATRegistrationNo(VATRegistrationNo: Text; CountryCode: Text): Text + begin + if (StrLen(VATRegistrationNo) >= 2) and + (StrPos(AlphabetTok, CopyStr(VATRegistrationNo, 1, 1)) > 0) and + (StrPos(AlphabetTok, CopyStr(VATRegistrationNo, 2, 1)) > 0) + then + exit(VATRegistrationNo); + + exit(CountryCode + VATRegistrationNo); + end; + + procedure TryGetBuyerElectronicAddress(Customer: Record Customer; var BuyerElectronicAddress: Text): Boolean + var + VATRegistrationNo: Text; + begin + if Customer."FR Electronic Address" <> '' then begin + BuyerElectronicAddress := Customer."FR Electronic Address"; + exit(true); + end; + + if Customer."Registration Number" <> '' then begin + BuyerElectronicAddress := CopyStr(Customer."Registration Number", 1, 14); + exit(true); + end; + + VATRegistrationNo := UpperCase(DelChr(Customer."VAT Registration No.", '=', ' ')); + if (StrLen(VATRegistrationNo) = 13) and (CopyStr(VATRegistrationNo, 1, 2) = 'FR') and + (DelChr(CopyStr(VATRegistrationNo, 3), '=', '0123456789') = '') + then begin + BuyerElectronicAddress := CopyStr(VATRegistrationNo, 5, 9); + exit(true); + end; + + exit(false); + end; + local procedure GetHeaderFieldText(var SourceDocumentHeader: RecordRef; PrimaryFieldName: Text; FallbackFieldName: Text): Text var FREDocHelpers: Codeunit "EDoc. Helpers"; @@ -295,20 +396,6 @@ codeunit 10978 "CII XML Builder" PartyElement.Add(ElecCommElement); end; - local procedure GetElecAddressSchemeCode(ElecAddressScheme: Enum "Electronic Address Scheme"): Text - begin - case ElecAddressScheme of - ElecAddressScheme::"EM": - exit('EM'); - ElecAddressScheme::"0009": - exit('0009'); - ElecAddressScheme::"0002": - exit('0002'); - else - exit(Format(ElecAddressScheme)); - end; - end; - local procedure AddVATRegistration(var PartyElement: XmlElement; VATRegistrationNo: Text) var TaxRegElement: XmlElement; @@ -649,12 +736,13 @@ codeunit 10978 "CII XML Builder" local procedure InsertTaxElement(var SettlementElement: XmlElement; CalculatedAmount: Text; BasisAmount: Text; CategoryCode: Text; RateApplicablePercent: Text; ZeroVAT: Boolean) var TradeTaxElement: XmlElement; + ExemptionReasonLbl: Label 'Exempt from VAT', Locked = true; begin TradeTaxElement := XmlElement.Create('ApplicableTradeTax', RamNamespaceTok); TradeTaxElement.Add(XmlElement.Create('CalculatedAmount', RamNamespaceTok, CalculatedAmount)); TradeTaxElement.Add(XmlElement.Create('TypeCode', RamNamespaceTok, 'VAT')); if ZeroVAT then - TradeTaxElement.Add(XmlElement.Create('ExemptionReason', RamNamespaceTok, 'VATEX-EU-O')); + TradeTaxElement.Add(XmlElement.Create('ExemptionReason', RamNamespaceTok, ExemptionReasonLbl)); TradeTaxElement.Add(XmlElement.Create('BasisAmount', RamNamespaceTok, BasisAmount)); TradeTaxElement.Add(XmlElement.Create('CategoryCode', RamNamespaceTok, CategoryCode)); TradeTaxElement.Add(XmlElement.Create('RateApplicablePercent', RamNamespaceTok, RateApplicablePercent)); @@ -1047,8 +1135,14 @@ codeunit 10978 "CII XML Builder" var RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100', Locked = true; RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; + QdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', Locked = true; UdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', Locked = true; + AlphabetTok: Label 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', Locked = true; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; + GoodsBillingModeTok: Label 'B1', Locked = true; + ItemLineTypeTok: Label 'Item', Locked = true; + MixedBillingModeTok: Label 'M1', Locked = true; + ServiceBillingModeTok: Label 'S1', Locked = true; RecoveryCostNoteTok: Label 'Indemnité forfaitaire pour frais de recouvrement en cas de retard de paiement : 40 €', Locked = true; LatePaymentPenaltyNoteTok: Label 'Taux des pénalités de retard : taux directeur (BCE) majoré de 10 points', Locked = true; EarlyPaymentDiscountNoteTok: Label 'Pas d''escompte pour paiement anticipé', Locked = true; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al index 16370d26867..529d45ae337 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/EDocHelpers.Codeunit.al @@ -73,6 +73,7 @@ codeunit 10991 "EDoc. Helpers" FRCIIXMLBuilder: Codeunit "CII XML Builder"; CustomerNoFieldRef: FieldRef; CustomerNo: Code[20]; + BuyerElectronicAddress: Text; begin if not FRCIIXMLBuilder.TryGetCustomerNoFieldRef(SourceDocumentHeader, CustomerNoFieldRef) then exit; @@ -84,7 +85,8 @@ codeunit 10991 "EDoc. Helpers" if not Customer.Get(CustomerNo) then exit; - if Customer."FR Electronic Address" = '' then + Customer.SetLoadFields("FR Electronic Address", "Registration Number", "VAT Registration No."); + if not FRCIIXMLBuilder.TryGetBuyerElectronicAddress(Customer, BuyerElectronicAddress) then Error(BuyerElectronicAddressRequiredErr, Customer."No."); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 1aa793cb737..f1f26405ef8 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -239,15 +239,23 @@ codeunit 10975 "FR E-Invoice Message Mgt." local procedure GetVATEntryGrossAmount(VATEntry: Record "VAT Entry"; CurrencyCode: Code[10]): Decimal var VATEntryCurrencyErrorInfo: ErrorInfo; + GrossAmount: Decimal; begin if VATEntry."Source Currency Code" = CurrencyCode then - exit(-(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount")); - if VATEntry."Source Currency Code" = '' then - exit(-(VATEntry.Base + VATEntry.Amount)); + GrossAmount := -(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount") + else + if VATEntry."Source Currency Code" = '' then + GrossAmount := -(VATEntry.Base + VATEntry.Amount) + else begin + VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); + VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); + Error(VATEntryCurrencyErrorInfo); + end; + + if (GrossAmount = 0) and IsVATEntryReportable(VATEntry) then + GrossAmount := -(VATEntry."Unrealized Base" + VATEntry."Unrealized Amount"); - VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); - VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); - Error(VATEntryCurrencyErrorInfo); + exit(GrossAmount); end; local procedure IsVATEntryReportable(VATEntry: Record "VAT Entry"): Boolean diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 12a79a125e0..de85ae3732c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -232,7 +232,6 @@ codeunit 148151 "FR E-Invoice Message Tests" asserterror EDocumentMessageAPI.SendMessage(MessageEntryNo); - Assert.ExpectedError('could not be sent'); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'The connector must be invoked before its missing success result is rejected.'); end; @@ -327,7 +326,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure CollectedMessageAllowsMissingSenderPlatformIdentity() var EDocument: Record "E-Document"; - EDocumentService: Record "E-Document Service"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; @@ -337,10 +335,7 @@ codeunit 148151 "FR E-Invoice Message Tests" Initialize(); // [GIVEN] An eligible payment whose service has no sender-platform ID - CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - EDocumentService.Get(EDocument.Service); - Clear(EDocumentService."FR Sender Platform ID"); - EDocumentService.Modify(); + CreatePaymentScenarioWithoutSenderPlatform(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment is processed FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); @@ -356,7 +351,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure CollectedMessageWithoutPlatformUsesCDVProfile() var EDocument: Record "E-Document"; - EDocumentService: Record "E-Document Service"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; @@ -366,10 +360,7 @@ codeunit 148151 "FR E-Invoice Message Tests" Initialize(); // [GIVEN] An eligible payment whose service has no sender-platform identity - CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - EDocumentService.Get(EDocument.Service); - Clear(EDocumentService."FR Sender Platform ID"); - EDocumentService.Modify(); + CreatePaymentScenarioWithoutSenderPlatform(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment is processed and its lifecycle message is sent FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); @@ -1454,8 +1445,9 @@ codeunit 148151 "FR E-Invoice Message Tests" SchemeNode: XmlNode; XmlNode: XmlNode; PartyPath: Text; + PartyPathTok: Label '//*[local-name()="ExchangedDocument"]/*[local-name()="%1"]', Locked = true; begin - PartyPath := StrSubstNo('//*[local-name()="ExchangedDocument"]/*[local-name()="%1"]', ElementName); + PartyPath := StrSubstNo(PartyPathTok, ElementName); Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]', XmlNode), 'The payload must contain the expected trade-party ID.'); Assert.AreEqual(ExpectedID, XmlNode.AsXmlElement().InnerText(), 'The trade-party ID is incorrect.'); Assert.IsTrue(XmlDoc.SelectSingleNode(PartyPath + '/*[local-name()="GlobalID"]/@schemeID', SchemeNode), 'The trade-party ID must contain a scheme.'); @@ -1536,6 +1528,16 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, ServiceStatus, 120); end; + local procedure CreatePaymentScenarioWithoutSenderPlatform(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status") + var + EDocumentService: Record "E-Document Service"; + begin + EDocumentService.Get('FR-MESSAGE-MOCK'); + Clear(EDocumentService."FR Sender Platform ID"); + EDocumentService.Modify(); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, ServiceStatus); + end; + local procedure CreatePaymentScenarioWithAmount(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status"; PaymentAmount: Decimal) var Customer: Record Customer; @@ -1549,7 +1551,7 @@ codeunit 148151 "FR E-Invoice Message Tests" PostedInvoiceNo: Code[20]; begin EDocumentService.Get('FR-MESSAGE-MOCK'); - EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + Clear(EDocumentService."Document Format"); EDocumentService.Modify(); LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); VATPostingSetup."Unrealized VAT Type" := VATPostingSetup."Unrealized VAT Type"::Percentage; @@ -1558,9 +1560,11 @@ codeunit 148151 "FR E-Invoice Message Tests" VATPostingSetup.Modify(true); LibrarySales.CreateCustomer(Customer); Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + PrepareCustomerForPosting(Customer); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + PrepareSalesHeaderForPosting(SalesHeader); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); SalesLine.Validate("Unit Price", 100); @@ -1611,7 +1615,7 @@ codeunit 148151 "FR E-Invoice Message Tests" PostedInvoiceNo: Code[20]; begin EDocumentService.Get('FR-MESSAGE-MOCK'); - EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + Clear(EDocumentService."Document Format"); EDocumentService.Modify(); LibraryERM.CreateVATPostingSetupWithAccounts(UnrealizedVATSetup, UnrealizedVATSetup."VAT Calculation Type"::"Normal VAT", 20); UnrealizedVATSetup."Unrealized VAT Type" := UnrealizedVATSetup."Unrealized VAT Type"::Percentage; @@ -1622,9 +1626,11 @@ codeunit 148151 "FR E-Invoice Message Tests" NormalVATSetup.Rename(UnrealizedVATSetup."VAT Bus. Posting Group", NormalVATSetup."VAT Prod. Posting Group"); LibrarySales.CreateCustomer(Customer); Customer.Validate("VAT Bus. Posting Group", UnrealizedVATSetup."VAT Bus. Posting Group"); + PrepareCustomerForPosting(Customer); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + PrepareSalesHeaderForPosting(SalesHeader); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", LibraryERM.CreateGLAccountWithVATPostingSetup(UnrealizedVATSetup, "General Posting Type"::Sale), 1); SalesLine.Validate("Unit Price", 100); @@ -1678,7 +1684,7 @@ codeunit 148151 "FR E-Invoice Message Tests" PostedInvoiceNo: Code[20]; begin EDocumentService.Get('FR-MESSAGE-MOCK'); - EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + Clear(EDocumentService."Document Format"); EDocumentService.Modify(); LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup20, VATSetup20."VAT Calculation Type"::"Normal VAT", 20); VATSetup20."Unrealized VAT Type" := VATSetup20."Unrealized VAT Type"::Percentage; @@ -1699,9 +1705,11 @@ codeunit 148151 "FR E-Invoice Message Tests" VATSetup7.Modify(true); LibrarySales.CreateCustomer(Customer); Customer.Validate("VAT Bus. Posting Group", VATSetup20."VAT Bus. Posting Group"); + PrepareCustomerForPosting(Customer); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + PrepareSalesHeaderForPosting(SalesHeader); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup20, "General Posting Type"::Sale), 1); SalesLine.Validate("Unit Price", 100); @@ -1757,14 +1765,16 @@ codeunit 148151 "FR E-Invoice Message Tests" PostedInvoiceNo: Code[20]; begin EDocumentService.Get('FR-MESSAGE-MOCK'); - EDocumentService."Document Format" := EDocumentService."Document Format"::Mock; + Clear(EDocumentService."Document Format"); EDocumentService.Modify(); LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 10); LibrarySales.CreateCustomer(Customer); Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + PrepareCustomerForPosting(Customer); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + PrepareSalesHeaderForPosting(SalesHeader); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); SalesLine.Validate("Unit Price", 100); @@ -1799,6 +1809,29 @@ codeunit 148151 "FR E-Invoice Message Tests" FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); end; + local procedure PrepareCustomerForPosting(var Customer: Record Customer) + begin + Customer.Validate(Address, 'Test Address'); + Customer.Validate(City, 'Paris'); + Customer.Validate("Post Code", '75001'); + Customer.Validate("Country/Region Code", 'FR'); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); + end; + + local procedure PrepareSalesHeaderForPosting(var SalesHeader: Record "Sales Header") + begin + SalesHeader.Validate("Bill-to Address", 'Test Address'); + SalesHeader.Validate("Bill-to City", 'Paris'); + SalesHeader.Validate("Bill-to Post Code", '75001'); + SalesHeader.Validate("Bill-to Country/Region Code", 'FR'); + SalesHeader.Validate("Ship-to Address", 'Test Address'); + SalesHeader.Validate("Ship-to City", 'Paris'); + SalesHeader.Validate("Ship-to Post Code", '75001'); + SalesHeader.Validate("Ship-to Country/Region Code", 'FR'); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); + end; + local procedure CreateServiceStatus(EDocument: Record "E-Document"; ServiceStatus: Enum "E-Document Service Status") var EDocumentServiceStatus: Record "E-Document Service Status"; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index e0af9ca3c46..05aed402402 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -12,6 +12,7 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; +using Microsoft.Foundation.Reporting; using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; using Microsoft.Inventory.Location; @@ -365,8 +366,8 @@ codeunit 148148 "Factur-X CII XML Tests" Initialize(); // [GIVEN] Sales invoice with a foreign Currency Code - LibraryERM.CreateCurrency(Currency); - LibraryERM.CreateRandomExchangeRate(Currency.Code); + EnsureCurrency(Currency, 'USD'); + EnsureExchangeRate(Currency.Code); SalesHeader.Get("Sales Document Type"::Invoice, CreateSalesDocumentWithLine("Sales Document Type"::Invoice, '', Currency.Code)); SalesInvoiceHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); @@ -793,7 +794,7 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); Customer.Modify(true); - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, Customer."No."); + CreateSalesDocument(SalesHeader, Customer."No."); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Modify(true); @@ -1142,8 +1143,8 @@ codeunit 148148 "Factur-X CII XML Tests" Initialize(); // [GIVEN] Sales credit memo with a foreign Currency Code - LibraryERM.CreateCurrency(Currency); - LibraryERM.CreateRandomExchangeRate(Currency.Code); + EnsureCurrency(Currency, 'USD'); + EnsureExchangeRate(Currency.Code); SalesHeader.Get("Sales Document Type"::"Credit Memo", CreateSalesDocumentWithLine("Sales Document Type"::"Credit Memo", '', Currency.Code)); SalesCrMemoHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, true, true)); @@ -1214,6 +1215,7 @@ codeunit 148148 "Factur-X CII XML Tests" procedure FacturXSalesCrMemoZeroVATCatSPreservedWithGermanBuyer() var Customer: Record Customer; + DocumentSendingProfile: Record "Document Sending Profile"; GLAccount: Record "G/L Account"; VATPostingSetup: Record "VAT Posting Setup"; SalesHeader: Record "Sales Header"; @@ -1233,9 +1235,19 @@ codeunit 148148 "Factur-X CII XML Tests" EnsureCountryRegionExists('DE'); LibrarySales.CreateCustomer(Customer); Customer.Validate("Country/Region Code", 'DE'); + Customer.Address := 'Test Address'; + Customer."Post Code" := '10115'; + Customer.City := 'Berlin'; Customer."VAT Registration No." := '533435789'; Customer."Registration Number" := ''; Customer."FR Electronic Address" := '123456789_FOREIGN'; + if not DocumentSendingProfile.Get('NON-EDOC') then begin + DocumentSendingProfile.Init(); + DocumentSendingProfile.Code := 'NON-EDOC'; + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::No; + DocumentSendingProfile.Insert(true); + end; + Customer.Validate("Document Sending Profile", DocumentSendingProfile.Code); Customer.Modify(true); CustomerNo := Customer."No."; @@ -1256,6 +1268,8 @@ codeunit 148148 "Factur-X CII XML Tests" // [GIVEN] Posted sales credit memo "CM" with a single financial line LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); @@ -1589,7 +1603,7 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); Customer.Validate("VAT Bus. Posting Group", FirstVATPostingSetup."VAT Bus. Posting Group"); Customer.Modify(true); - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + CreateSalesDocument(SalesHeader, CustomerNo); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 200); SalesLine.Validate("Allow Invoice Disc.", true); @@ -1820,6 +1834,13 @@ codeunit 148148 "Factur-X CII XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; + local procedure CreateSalesDocument(var SalesHeader: Record "Sales Header"; CustomerNo: Code[20]) + begin + LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); + end; + local procedure CreateAndPostSalesInvoiceForCustomer(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; @@ -1837,7 +1858,7 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); Customer.Modify(true); - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + CreateSalesDocument(SalesHeader, CustomerNo); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Modify(true); @@ -1875,7 +1896,7 @@ codeunit 148148 "Factur-X CII XML Tests" Item.Get(LibraryInventory.CreateItemNoWithPostingSetup( GLAccount."Gen. Prod. Posting Group", GLAccount."VAT Prod. Posting Group")); LibraryInventory.UpdateInventoryPostingSetup(Location, Item."Inventory Posting Group"); - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + CreateSalesDocument(SalesHeader, CustomerNo); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Modify(true); @@ -1938,6 +1959,7 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); Customer.Modify(true); LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::"Credit Memo", Customer."No."); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); SalesHeader.Validate("Applies-to Doc. Type", SalesHeader."Applies-to Doc. Type"::Invoice); SalesHeader.Validate("Applies-to Doc. No.", SalesInvoiceHeader."No."); SalesHeader.Modify(true); @@ -1974,12 +1996,31 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Get(CustomerNo); Customer.Validate("Gen. Bus. Posting Group", GLAccount."Gen. Bus. Posting Group"); Customer.Validate("VAT Bus. Posting Group", GLAccount."VAT Bus. Posting Group"); + if Customer.Address = '' then + Customer.Address := CopyStr(LibraryUtility.GenerateRandomText(MaxStrLen(Customer.Address)), 1, MaxStrLen(Customer.Address)); + if Customer."Post Code" = '' then + Customer.Validate("Post Code", '75001'); Customer.Modify(true); - LibrarySales.CreateSalesHeader(SalesHeader, DocType, CustomerNo); - if CurrencyCode <> '' then begin - SalesHeader.Validate("Currency Code", CurrencyCode); + if DocType = "Sales Document Type"::Invoice then + CreateSalesDocument(SalesHeader, CustomerNo) + else begin + LibrarySales.CreateSalesHeader(SalesHeader, DocType, CustomerNo); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); SalesHeader.Modify(true); end; + if SalesHeader."Bill-to City" = '' then + SalesHeader.Validate("Bill-to City", 'Paris'); + if SalesHeader."Bill-to Post Code" = '' then + SalesHeader.Validate("Bill-to Post Code", '75001'); + if SalesHeader."Ship-to City" = '' then + SalesHeader.Validate("Ship-to City", SalesHeader."Bill-to City"); + if SalesHeader."Ship-to Post Code" = '' then + SalesHeader.Validate("Ship-to Post Code", '75001'); + if SalesHeader."Ship-to Country/Region Code" = '' then + SalesHeader.Validate("Ship-to Country/Region Code", CompanyInformation."Country/Region Code"); + if CurrencyCode <> '' then + SalesHeader.Validate("Currency Code", CurrencyCode); + SalesHeader.Modify(true); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 100); SalesLine.Validate("Unit of Measure Code", GetUnitOfMeasureCode()); @@ -2014,12 +2055,13 @@ codeunit 148148 "Factur-X CII XML Tests" Customer.Modify(true); if ApplyInvoiceDiscount then begin + EnsureSalesInvoiceDiscountAccount(GLAccount."Gen. Bus. Posting Group", GLAccount."Gen. Prod. Posting Group"); LibraryERM.CreateInvDiscForCustomer(CustInvoiceDisc, CustomerNo, '', 0); CustInvoiceDisc.Validate("Discount %", 10); CustInvoiceDisc.Modify(true); end; - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + CreateSalesDocument(SalesHeader, CustomerNo); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 200); SalesLine.Validate("Allow Invoice Disc.", true); @@ -2071,7 +2113,7 @@ codeunit 148148 "Factur-X CII XML Tests" CustInvoiceDisc.Validate("Discount %", 10); CustInvoiceDisc.Modify(true); - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); + CreateSalesDocument(SalesHeader, CustomerNo); LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", GLAccount."No.", 1); SalesLine.Validate("Unit Price", 500); SalesLine.Validate("Allow Invoice Disc.", true); @@ -2116,6 +2158,9 @@ codeunit 148148 "Factur-X CII XML Tests" begin LibrarySales.CreateCustomer(Customer); Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); + Customer.Address := CopyStr(LibraryUtility.GenerateRandomText(MaxStrLen(Customer.Address)), 1, MaxStrLen(Customer.Address)); + Customer.Validate("Post Code", '75001'); + Customer.City := 'Paris'; Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); Customer."Registration Number" := '123456789'; Customer.Validate("FR Electronic Address", FRElecAddress); @@ -2123,12 +2168,45 @@ codeunit 148148 "Factur-X CII XML Tests" exit(Customer."No."); end; + local procedure EnsureCurrency(var Currency: Record Currency; CurrencyCode: Code[10]) + begin + if Currency.Get(CurrencyCode) then + exit; + + Currency.Init(); + Currency.Code := CurrencyCode; + Currency.Insert(true); + end; + + local procedure EnsureExchangeRate(CurrencyCode: Code[10]) + var + CurrencyExchangeRate: Record "Currency Exchange Rate"; + begin + CurrencyExchangeRate.SetRange("Currency Code", CurrencyCode); + CurrencyExchangeRate.SetFilter("Starting Date", '..%1', WorkDate()); + if not CurrencyExchangeRate.IsEmpty() then + exit; + + LibraryERM.CreateRandomExchangeRate(CurrencyCode); + end; + local procedure CreateCustomerWithoutIdentifiers(): Code[20] var Customer: Record Customer; + DocumentSendingProfile: Record "Document Sending Profile"; begin LibrarySales.CreateCustomer(Customer); Customer.Validate("Country/Region Code", CompanyInformation."Country/Region Code"); + Customer.Address := CopyStr(LibraryUtility.GenerateRandomText(MaxStrLen(Customer.Address)), 1, MaxStrLen(Customer.Address)); + Customer.Validate("Post Code", '75001'); + Customer.City := 'Paris'; + if not DocumentSendingProfile.Get('NON-EDOC') then begin + DocumentSendingProfile.Init(); + DocumentSendingProfile.Code := 'NON-EDOC'; + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::No; + DocumentSendingProfile.Insert(true); + end; + Customer.Validate("Document Sending Profile", DocumentSendingProfile.Code); Customer."FR Electronic Address" := ''; Clear(Customer."FR Elec. Address Scheme"); Customer."VAT Registration No." := ''; @@ -2378,6 +2456,10 @@ codeunit 148148 "Factur-X CII XML Tests" UnitOfMeasure.Description := 'Each'; UnitOfMeasure.Insert(true); end; + if UnitOfMeasure."International Standard Code" <> 'C62' then begin + UnitOfMeasure.Validate("International Standard Code", 'C62'); + UnitOfMeasure.Modify(true); + end; exit(UnitOfMeasure.Code); end; From 62dd4e4c7da176e74a82e3f7685f2faef34f4e24 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 24 Aug 2026 12:13:40 +0200 Subject: [PATCH 18/49] compile errors --- .../app/src/Core/CIIXMLBuilder.Codeunit.al | 45 ------------------- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 12 ++--- 2 files changed, 6 insertions(+), 51 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al index e735fe0c7f3..ae15505aee8 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al @@ -91,34 +91,6 @@ codeunit 10978 "CII XML Builder" OnAfterAddExchangedDocumentContext(RootElement, SourceDocumentLines); end; - local procedure GetBillingMode(var SourceDocumentLines: RecordRef): Text - var - FREDocHelpers: Codeunit "EDoc. Helpers"; - TypeFieldRef: FieldRef; - HasItemLine: Boolean; - HasServiceLine: Boolean; - LineType: Text; - begin - if not FREDocHelpers.FindFieldByName(SourceDocumentLines, 'Type', TypeFieldRef) then - exit(ServiceBillingModeTok); - - if SourceDocumentLines.FindSet() then - repeat - LineType := DelChr(Format(TypeFieldRef.Value()), '=', ' '); - if LineType = ItemLineTypeTok then - HasItemLine := true - else - if LineType <> '' then - HasServiceLine := true; - until SourceDocumentLines.Next() = 0; - - if HasItemLine and HasServiceLine then - exit(MixedBillingModeTok); - if HasItemLine then - exit(GoodsBillingModeTok); - exit(ServiceBillingModeTok); - end; - local procedure AddExchangedDocument(var RootElement: XmlElement; var EDocument: Record "E-Document"; TypeCode: Text) var DocElement: XmlElement; @@ -326,17 +298,6 @@ codeunit 10978 "CII XML Builder" AgreementElement.Add(BuyerElement); end; - local procedure NormalizeBuyerVATRegistrationNo(VATRegistrationNo: Text; CountryCode: Text): Text - begin - if (StrLen(VATRegistrationNo) >= 2) and - (StrPos(AlphabetTok, CopyStr(VATRegistrationNo, 1, 1)) > 0) and - (StrPos(AlphabetTok, CopyStr(VATRegistrationNo, 2, 1)) > 0) - then - exit(VATRegistrationNo); - - exit(CountryCode + VATRegistrationNo); - end; - procedure TryGetBuyerElectronicAddress(Customer: Record Customer; var BuyerElectronicAddress: Text): Boolean var VATRegistrationNo: Text; @@ -842,7 +803,6 @@ codeunit 10978 "CII XML Builder" local procedure InsertTaxElement(var SettlementElement: XmlElement; CalculatedAmount: Text; BasisAmount: Text; CategoryCode: Text; RateApplicablePercent: Text; VATEXCode: Text) var TradeTaxElement: XmlElement; - ExemptionReasonLbl: Label 'Exempt from VAT', Locked = true; begin TradeTaxElement := XmlElement.Create('ApplicableTradeTax', RamNamespaceTok); TradeTaxElement.Add(XmlElement.Create('CalculatedAmount', RamNamespaceTok, CalculatedAmount)); @@ -1263,12 +1223,7 @@ codeunit 10978 "CII XML Builder" RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; QdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:QualifiedDataType:100', Locked = true; UdtNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100', Locked = true; - AlphabetTok: Label 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', Locked = true; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; - GoodsBillingModeTok: Label 'B1', Locked = true; - ItemLineTypeTok: Label 'Item', Locked = true; - MixedBillingModeTok: Label 'M1', Locked = true; - ServiceBillingModeTok: Label 'S1', Locked = true; RecoveryCostNoteTok: Label 'Indemnité forfaitaire pour frais de recouvrement en cas de retard de paiement : 40 €', Locked = true; LatePaymentPenaltyNoteTok: Label 'Taux des pénalités de retard : taux directeur (BCE) majoré de 10 points', Locked = true; EarlyPaymentDiscountNoteTok: Label 'Pas d''escompte pour paiement anticipé', Locked = true; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index f1f26405ef8..b6723a5185e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -241,16 +241,16 @@ codeunit 10975 "FR E-Invoice Message Mgt." VATEntryCurrencyErrorInfo: ErrorInfo; GrossAmount: Decimal; begin - if VATEntry."Source Currency Code" = CurrencyCode then - GrossAmount := -(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount") - else - if VATEntry."Source Currency Code" = '' then + case true of + VATEntry."Source Currency Code" = CurrencyCode: + GrossAmount := -(VATEntry."Source Currency VAT Base" + VATEntry."Source Currency VAT Amount"); + VATEntry."Source Currency Code" = '': GrossAmount := -(VATEntry.Base + VATEntry.Amount) - else begin + else VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); Error(VATEntryCurrencyErrorInfo); - end; + end; if (GrossAmount = 0) and IsVATEntryReportable(VATEntry) then GrossAmount := -(VATEntry."Unrealized Base" + VATEntry."Unrealized Amount"); From ad98e96d42c200cdb7cd38c5f27312f703c83370 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 24 Aug 2026 14:08:55 +0200 Subject: [PATCH 19/49] tests update --- .../test/src/FREInvoiceMessageTests.Codeunit.al | 7 ++++--- .../EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al | 8 -------- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index de85ae3732c..a6e69ae428b 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -539,6 +539,7 @@ codeunit 148151 "FR E-Invoice Message Tests" ReversalVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + SalesInvoiceLine: Record "Sales Invoice Line"; VATPostingSetup: Record "VAT Posting Setup"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin @@ -557,9 +558,9 @@ codeunit 148151 "FR E-Invoice Message Tests" OriginalVAT.FindFirst(); // [GIVEN] VAT and sender-platform setup are changed after the original message - VATPostingSetup.SetRange("Tax Category", 'S'); - VATPostingSetup.SetRange("Unrealized VAT Type", VATPostingSetup."Unrealized VAT Type"::Percentage); - VATPostingSetup.FindFirst(); + SalesInvoiceLine.SetRange("Document No.", EDocument."Document No."); + SalesInvoiceLine.FindFirst(); + VATPostingSetup.Get(SalesInvoiceLine."VAT Bus. Posting Group", SalesInvoiceLine."VAT Prod. Posting Group"); VATPostingSetup."Tax Category" := 'Z'; VATPostingSetup.Modify(); EDocumentService.Get(EDocument.Service); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index b7579a54575..30e48c1aa70 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -51,7 +51,6 @@ codeunit 148148 "Factur-X CII XML Tests" EDocHelpers: Codeunit "EDoc. Helpers"; FacturXFormat: Codeunit "Factur-X Format"; IncorrectValueErr: Label 'Incorrect value for %1', Comment = '%1 = XML element path', Locked = true; - BuyerElectronicAddressRequiredErr: Label 'Electronic Address must be specified for Customer %1 for French e-invoicing.', Comment = '%1 = Customer No.'; FacturXProfileIdTok: Label 'urn:cen.eu:en16931:2017', Locked = true; DialogErrorCodeTok: Label 'Dialog', Locked = true; IsInitialized: Boolean; @@ -1850,13 +1849,6 @@ codeunit 148148 "Factur-X CII XML Tests" exit(LibrarySales.PostSalesDocument(SalesHeader, true, true)); end; - local procedure CreateSalesDocument(var SalesHeader: Record "Sales Header"; CustomerNo: Code[20]) - begin - LibrarySales.CreateSalesHeader(SalesHeader, "Sales Document Type"::Invoice, CustomerNo); - SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); - SalesHeader.Modify(true); - end; - local procedure CreateAndPostSalesInvoiceForCustomer(CustomerNo: Code[20]): Code[20] var Customer: Record Customer; From bb53620599405816dcee79ef23b2caac3690cef2 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 24 Aug 2026 23:07:53 +0200 Subject: [PATCH 20/49] tests updated --- .../src/EDocFRStructImportTests.Codeunit.al | 24 ++-- .../src/ExportEReportingTests.Codeunit.al | 1 - .../src/FREInvoiceMessageTests.Codeunit.al | 120 +++++++++++++++++- .../test/src/FacturXCIIXMLTests.Codeunit.al | 11 +- .../test/src/IdentificationTests.Codeunit.al | 1 - .../test/src/PEPPOLBIS30XMLTests.Codeunit.al | 72 ++++++++++- 6 files changed, 205 insertions(+), 24 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al index 4c4842b88ed..35a7a58905e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al @@ -33,11 +33,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Factur-X invoice is read into a purchase invoice draft + Initialize(); // [GIVEN] A Factur-X CII invoice - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -82,11 +82,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Factur-X credit memo is read into a purchase credit memo draft + Initialize(); // [GIVEN] A Factur-X CII credit memo - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -105,11 +105,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocument: Record "E-Document"; EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] Reading a document that is not a Cross Industry Invoice fails with a clear error + Initialize(); // [GIVEN] An XML document with an unsupported root element - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -126,11 +126,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocumentPurchaseLine: Record "E-Document Purchase Line"; EDocumentFacturXHandler: Codeunit "E-Document Factur-X Handler"; begin - // [FEATURE] [E-Document] [Factur-X] [Import] + // [FEATURE] [AI test] // [SCENARIO] Re-running Read into Draft replaces the previous draft instead of duplicating it + Initialize(); // [GIVEN] A Factur-X invoice that has been read into draft - Initialize(); CreateEDocument(EDocument); EDocumentFacturXHandler.ReadIntoDraft(EDocument, GetResourceBlob(FacturXInvoiceTok)); @@ -153,11 +153,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; ProcessDraft: Enum "E-Doc. Process Draft"; begin - // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] + // [FEATURE] [AI test] // [SCENARIO] A Peppol BIS 3.0 FR invoice is read into a purchase invoice draft + Initialize(); // [GIVEN] A Peppol BIS 3.0 FR UBL invoice - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft @@ -197,11 +197,11 @@ codeunit 148149 "E-Doc. FR Struct. Import Tests" EDocument: Record "E-Document"; EDocPeppolBIS30FRHandler: Codeunit "E-Doc. Peppol BIS 3.0 FR Hdlr"; begin - // [FEATURE] [E-Document] [Peppol BIS 3.0 FR] [Import] + // [FEATURE] [AI test] // [SCENARIO] Reading a document that is neither an Invoice nor a CreditNote fails with a clear error + Initialize(); // [GIVEN] An XML document with an unsupported root element - Initialize(); CreateEDocument(EDocument); // [WHEN] The document is read into draft diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al index 4e1199f0318..2c062954f35 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al @@ -28,7 +28,6 @@ codeunit 148145 "Export E-Reporting Tests" trigger OnRun() begin - // [FEATURE] [E-Reporting FR E-document] end; var diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index a6e69ae428b..fe75b9d3815 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -142,7 +142,11 @@ codeunit 148151 "FR E-Invoice Message Tests" NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] Unapplying a payment creates a negative Collected message linked to the original Initialize(); + + // [GIVEN] An approved E-Document with a sent Collected message CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -151,8 +155,10 @@ codeunit 148151 "FR E-Invoice Message Tests" SendMessage(CollectedMessage); CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); + // [WHEN] The payment is unapplied FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + // [THEN] A Negative Collected message linked to the original is created; NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); NegativeMessage.SetRange(Type, NegativeMessage.Type::"Negative Collected"); NegativeMessage.FindFirst(); @@ -178,11 +184,17 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] Refusing an invoice sends a lifecycle message with status 210 and reason code Initialize(); + + // [GIVEN] An incoming French E-Document CreateIncomingEDocument(EDocument); + // [WHEN] The invoice is refused with a reason FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'PRICE', 'The amount is incorrect.'); + // [THEN] A refusal message with status 210 and the reason code is queued and can be sent Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Refusal must queue the message without invoking the connector.'); SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One refusal message must be sent.'); @@ -222,7 +234,11 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; MessageEntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] Sending a message without the connector reporting success raises an error Initialize(); + + // [GIVEN] An incoming E-Document with a lifecycle message and a mock connector that does not report success CreateIncomingEDocument(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); @@ -230,8 +246,10 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, TempBlob); MessageSenderMock.SetReportSuccess(false); + // [WHEN] The message is sent asserterror EDocumentMessageAPI.SendMessage(MessageEntryNo); + // [THEN] The connector is invoked before its missing success result is rejected Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'The connector must be invoked before its missing success result is rejected.'); end; @@ -244,12 +262,18 @@ codeunit 148151 "FR E-Invoice Message Tests" DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] Processing the same payment application twice is idempotent Initialize(); + + // [GIVEN] An approved French E-Document with an applied payment CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + // [WHEN] The payment application is processed twice FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + // [THEN] Only one payment occurrence and one Collected message exist; EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); Assert.RecordCount(EDocPaymentOccurrence, 1); @@ -640,7 +664,11 @@ codeunit 148151 "FR E-Invoice Message Tests" FirstMessageEntryNo: Integer; DuplicateMessageEntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] An incoming lifecycle message is correlated to its E-Document and deduplicated by external ID Initialize(); + + // [GIVEN] An incoming E-Document with a registered external document reference CreateIncomingEDocument(EDocument); ExternalDocumentID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalDocumentID)); ExternalMessageID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalMessageID)); @@ -648,6 +676,7 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); + // [WHEN] The same incoming message is received twice FirstMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage( EDocument.Service, ExternalDocumentID, ExternalMessageID, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); @@ -655,6 +684,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument.Service, ExternalDocumentID, ExternalMessageID, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); + // [THEN] The incoming message is persisted and deduplicated Assert.AreNotEqual(0, FirstMessageEntryNo, 'The incoming lifecycle message must be persisted.'); Assert.AreEqual(FirstMessageEntryNo, DuplicateMessageEntryNo, 'The external message ID must deduplicate repeated delivery.'); end; @@ -667,15 +697,21 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; begin + // [FEATURE] [AI test] + // [SCENARIO] Creating an incoming message fails when the external document reference is not registered Initialize(); + + // [GIVEN] An incoming E-Document without a registered external document reference CreateIncomingEDocument(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); + // [WHEN] An incoming message with an unregistered external document ID is created asserterror EDocumentMessageAPI.CreateIncomingMessage( EDocument.Service, CopyStr(Format(CreateGuid()), 1, 250), CopyStr(Format(CreateGuid()), 1, 250), "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, CurrentDateTime(), TempBlob); + // [THEN] An error about unregistered reference is raised Assert.ExpectedError('is not registered'); end; @@ -686,11 +722,17 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] Accepting an invoice queues an Accepted message with status 205 Initialize(); + + // [GIVEN] An incoming French E-Document CreateIncomingEDocument(EDocument); + // [WHEN] The invoice is accepted FREInvoiceMessageMgt.AcceptInvoice(EDocument); + // [THEN] An Accepted message with status 205 is queued and can be sent FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Accepted); Assert.RecordCount(FREInvoiceMessage, 1); @@ -707,11 +749,18 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] A buyer response cannot follow a previous acceptance Initialize(); - CreateIncomingEDocument(EDocument); + // [GIVEN] An incoming E-Document that has been accepted + CreateIncomingEDocument(EDocument); FREInvoiceMessageMgt.AcceptInvoice(EDocument); + + // [WHEN] A refusal is attempted after acceptance asserterror FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Changed my mind.'); + + // [THEN] An error about duplicate buyer response is raised; Assert.ExpectedError('already has a buyer response'); Assert.ExpectedErrorCode('Dialog'); end; @@ -722,11 +771,18 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin + // [FEATURE] [AI test] + // [SCENARIO] A buyer response cannot follow a previous refusal Initialize(); - CreateIncomingEDocument(EDocument); + // [GIVEN] An incoming E-Document that has been refused + CreateIncomingEDocument(EDocument); FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'OTHER', 'Not accepted.'); + + // [WHEN] An acceptance is attempted after refusal asserterror FREInvoiceMessageMgt.AcceptInvoice(EDocument); + + // [THEN] An error about duplicate buyer response is raised; Assert.ExpectedError('already has a buyer response'); Assert.ExpectedErrorCode('Dialog'); end; @@ -745,7 +801,11 @@ codeunit 148151 "FR E-Invoice Message Tests" ReceivedAt: DateTime; FREntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving a Submitted lifecycle message persists a normalized FR message Initialize(); + + // [GIVEN] An outgoing E-Document with a registered external document reference and a Submitted lifecycle payload CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); @@ -754,8 +814,10 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); + // [WHEN] The Submitted lifecycle message is received FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, ReceivedAt, TempBlob); + // [THEN] The FR message is persisted with Submitted type and correct metadata FREInvoiceMessage.Get(FREntryNo); Assert.AreEqual(FREInvoiceMessage.Type::Submitted, FREInvoiceMessage.Type, 'FR type must be Submitted.'); Assert.AreEqual(ExternalMsgID, FREInvoiceMessage."External Message ID", 'External message ID must be stored.'); @@ -778,7 +840,11 @@ codeunit 148151 "FR E-Invoice Message Tests" ExternalMsgID: Text[250]; FREntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving an Accepted lifecycle message persists a normalized FR message Initialize(); + + // [GIVEN] An outgoing E-Document with a received Submitted status and an Accepted lifecycle payload CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); @@ -787,8 +853,10 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", '205', '', '')); + // [WHEN] The Accepted lifecycle message is received FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + // [THEN] The FR message is persisted with Accepted type FREInvoiceMessage.Get(FREntryNo); Assert.AreEqual(FREInvoiceMessage.Type::Accepted, FREInvoiceMessage.Type, 'FR type must be Accepted.'); Assert.AreEqual("E-Doc. Response Type"::Accepted, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'Generic response must be Accepted.'); @@ -807,7 +875,11 @@ codeunit 148151 "FR E-Invoice Message Tests" ExternalMsgID: Text[250]; FREntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving a Technical Rejected lifecycle message persists the reason code and description Initialize(); + + // [GIVEN] An outgoing E-Document with a received Submitted status and a Rejected lifecycle payload with reason CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); @@ -816,8 +888,10 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejetée', 'SCHEMA', 'Schema validation failed')); + // [WHEN] The Rejected lifecycle message is received FREntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + // [THEN] The FR message is persisted with Technical Rejected type and reason FREInvoiceMessage.Get(FREntryNo); Assert.AreEqual(FREInvoiceMessage.Type::"Technical Rejected", FREInvoiceMessage.Type, 'FR type must be Technical Rejected.'); Assert.AreEqual('SCHEMA', Format(FREInvoiceMessage."Reason Code"), 'Reason code must be persisted.'); @@ -839,7 +913,11 @@ codeunit 148151 "FR E-Invoice Message Tests" FirstEntryNo: Integer; SecondEntryNo: Integer; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving the same lifecycle message twice returns the same entry Initialize(); + + // [GIVEN] An outgoing E-Document with a registered external document reference CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); @@ -847,11 +925,13 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); + // [WHEN] The same lifecycle message is received twice FirstEntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); SecondEntryNo := FREInvoiceMessageAPI.ReceiveMessage(EDocument.Service, ExternalDocID, ExternalMsgID, CurrentDateTime(), TempBlob); + // [THEN] The same FR entry is returned and only one Submitted message exists Assert.AreEqual(FirstEntryNo, SecondEntryNo, 'Same external message ID must return same FR entry.'); FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Submitted); @@ -868,16 +948,22 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; ExternalDocID: Text[250]; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving a lifecycle message with invalid XML raises an error Initialize(); + + // [GIVEN] An outgoing E-Document with a registered reference and invalid XML payload CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText('not xml at all'); + // [WHEN] The invalid message is received asserterror FREInvoiceMessageAPI.ReceiveMessage( EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + // [THEN] An error about invalid XML is raised; Assert.ExpectedError('not valid XML'); Assert.ExpectedErrorCode('Dialog'); end; @@ -892,16 +978,22 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; ExternalDocID: Text[250]; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving a lifecycle message with an unsupported status raises an error Initialize(); + + // [GIVEN] An outgoing E-Document with a lifecycle payload containing an unknown status CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Unknown', '', '')); + // [WHEN] The message with unsupported status is received asserterror FREInvoiceMessageAPI.ReceiveMessage( EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + // [THEN] An error about unsupported status is raised; Assert.ExpectedError('is not supported'); Assert.ExpectedErrorCode('Dialog'); end; @@ -916,16 +1008,22 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; ExternalDocID: Text[250]; begin + // [FEATURE] [AI test] + // [SCENARIO] Receiving a lifecycle message whose invoice ID does not match the E-Document raises an error Initialize(); + + // [GIVEN] An outgoing E-Document with a lifecycle payload referencing a different invoice CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml('WRONG-INVOICE-ID', 'Submitted', '', '')); + // [WHEN] The mismatched message is received asserterror FREInvoiceMessageAPI.ReceiveMessage( EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + // [THEN] An error about invoice ID mismatch is raised; Assert.ExpectedError('does not match'); Assert.ExpectedErrorCode('Dialog'); end; @@ -940,16 +1038,22 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; ExternalDocID: Text[250]; begin + // [FEATURE] [AI test] + // [SCENARIO] A Technical Rejected message without a reason code is rejected Initialize(); + + // [GIVEN] An outgoing E-Document with a Rejected lifecycle payload missing the reason code CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', '', 'Something went wrong')); + // [WHEN] The Rejected message without reason code is received asserterror FREInvoiceMessageAPI.ReceiveMessage( EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + // [THEN] An error about missing reason code is raised; Assert.ExpectedError('reason code is required'); Assert.ExpectedErrorCode('Dialog'); end; @@ -964,16 +1068,22 @@ codeunit 148151 "FR E-Invoice Message Tests" OutStream: OutStream; ExternalDocID: Text[250]; begin + // [FEATURE] [AI test] + // [SCENARIO] A Technical Rejected message without a reason description is rejected Initialize(); + + // [GIVEN] An outgoing E-Document with a Rejected lifecycle payload missing the reason description CreateOutgoingEDocument(EDocument); ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', 'SCHEMA', '')); + // [WHEN] The Rejected message without reason description is received asserterror FREInvoiceMessageAPI.ReceiveMessage( EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob); + // [THEN] An error about missing reason description is raised; Assert.ExpectedError('reason description is required'); Assert.ExpectedErrorCode('Dialog'); end; @@ -1231,7 +1341,11 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessageBuilder: Codeunit "FR E-Invoice Message Builder"; TempBlob: Codeunit "Temp Blob"; begin + // [FEATURE] [AI test] + // [SCENARIO] Building a message with an incoming-only status raises an error Initialize(); + + // [GIVEN] An incoming E-Document with a Submitted lifecycle message CreateIncomingEDocument(EDocument); FREInvoiceMessage.Init(); FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; @@ -1241,8 +1355,10 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage."Created At" := CurrentDateTime(); FREInvoiceMessage.Insert(); + // [WHEN] The message builder attempts to build the message asserterror FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); + // [THEN] An error about unsendable status is raised; Assert.ExpectedError('cannot be sent'); Assert.ExpectedErrorCode('Dialog'); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index 30e48c1aa70..241c18fc2ee 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -35,7 +35,6 @@ codeunit 148148 "Factur-X CII XML Tests" trigger OnRun() begin - // [FEATURE] [Factur-X FR E-document] end; var @@ -186,6 +185,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML has seller name from Company Information Initialize(); + // [GIVEN] Posted sales invoice // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -204,6 +204,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML has seller VAT registration number with scheme VA Initialize(); + // [GIVEN] Posted sales invoice / Company information with VAT Registration No. // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -452,6 +453,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML has seller electronic address (BT-34) as SIRET with schemeID 0009 Initialize(); + // [GIVEN] Posted sales invoice // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -710,6 +712,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML has SpecifiedTradeSettlementPaymentMeans with TypeCode 58 (SEPA credit transfer) Initialize(); + // [GIVEN] Posted sales invoice // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -940,6 +943,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML line BilledQuantity has unitCode attribute (BT-130) Initialize(); + // [GIVEN] Posted sales invoice // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -958,6 +962,7 @@ codeunit 148148 "Factur-X CII XML Tests" // [SCENARIO] Factur-X CII XML line-level ApplicableTradeTax has TypeCode = 'VAT' Initialize(); + // [GIVEN] Posted sales invoice // [WHEN] Create CII XML CreateSalesInvoiceCIIXML(TempBlob); @@ -1327,7 +1332,7 @@ codeunit 148148 "Factur-X CII XML Tests" SourceDocumentHeader: RecordRef; SourceDocumentLines: RecordRef; begin - // [FEATURE] [Reminder] + // [FEATURE] [AI test] // [SCENARIO] An issued reminder line (which has no Quantity field) emits BilledQuantity = 1 Initialize(); @@ -1367,7 +1372,7 @@ codeunit 148148 "Factur-X CII XML Tests" SourceDocumentHeader: RecordRef; SourceDocumentLines: RecordRef; begin - // [FEATURE] [Finance Charge Memo] + // [FEATURE] [AI test] // [SCENARIO] An issued finance charge memo line (which has no Quantity field) emits BilledQuantity = 1 Initialize(); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al index 0e046f424c2..813e6f909e9 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/IdentificationTests.Codeunit.al @@ -14,7 +14,6 @@ codeunit 148146 "Identification Tests" trigger OnRun() begin - // [FEATURE] [FR Identification] end; var diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al index 6fbca804f04..f6c74a69591 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/PEPPOLBIS30XMLTests.Codeunit.al @@ -38,7 +38,6 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" trigger OnRun() begin - // [FEATURE] [PEPPOL BIS 3.0 FR E-document] end; var @@ -366,7 +365,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin - // [FEATURE] [AI test 0.4] + // [FEATURE] [AI test] // [SCENARIO] Exporting a sales invoice does not add a credit note billing reference Initialize(); @@ -387,13 +386,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] An invoice containing only service lines uses billing mode S1 Initialize(); + // [GIVEN] Posted sales invoice "SI" with service lines only SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('', "Electronic Address Scheme"::"EM"))); + // [WHEN] Sales invoice "SI" is exported in PEPPOL BIS 3.0 FR ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] ProfileID = 'S1' Assert.AreEqual('S1', GetNodeByPath(XmlDoc, '/Invoice/cbc:ProfileID'), StrSubstNo(IncorrectValueErr, 'ProfileID')); end; @@ -407,9 +410,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CustomerNo: Code[20]; EndpointId: Text[200]; begin + // [FEATURE] [AI test] // [SCENARIO] A service-specific routing identifier overrides the endpoint on the customer card Initialize(); + // [GIVEN] Customer "C" with service participant using scheme 0225 CustomerNo := CreateCustomer('12345678901234', "Electronic Address Scheme"::"0009"); EndpointId := '123456789_001'; ServiceParticipant.Service := EDocumentService.Code; @@ -426,9 +431,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Customer."FR Elec. Address Scheme" := Customer."FR Elec. Address Scheme"::" "; Customer.Modify(true); + // [WHEN] Sales invoice is checked and exported in PEPPOL BIS 3.0 FR CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] Buyer EndpointID uses service participant value with scheme 0225 Assert.AreEqual(EndpointId, GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Buyer EndpointID')); @@ -536,9 +543,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; CommentText: Text[80]; begin + // [FEATURE] [AI test] // [SCENARIO] A French regulatory comment on a posted credit memo is prefixed with its type in a UBL header note Initialize(); + // [GIVEN] Posted sales credit memo "SCM" with an AAB regulatory comment SalesCrMemoHeader.Get(CreateAndPostSalesCrMemo(CreateCustomer('', "Electronic Address Scheme"::"EM"))); CommentText := 'No discount is granted for early payment.'; SalesCommentLine."Document Type" := SalesCommentLine."Document Type"::"Posted Credit Memo"; @@ -548,8 +557,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesCommentLine.Comment := CommentText; SalesCommentLine.Insert(); + // [WHEN] Sales credit memo "SCM" is exported in PEPPOL BIS 3.0 FR ExportCrMemo(SalesCrMemoHeader, XmlDoc); + // [THEN] The AAB regulatory comment is exported as a UBL header note Assert.AreEqual('#AAB#' + CommentText, GetNodeByPath(XmlDoc, '/CreditNote/cbc:Note'), StrSubstNo(IncorrectValueErr, 'Note')); end; @@ -561,7 +572,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; CustomerNo: Code[20]; begin - // [FEATURE] [AI test 0.4] + // [FEATURE] [AI test] // [SCENARIO] A sales credit memo applied to an invoice exports the invoice number and issue date Initialize(); @@ -590,7 +601,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesCrMemoHeader: Record "Sales Cr.Memo Header"; XmlDoc: XmlDocument; begin - // [FEATURE] [AI test 0.4] + // [FEATURE] [AI test] // [SCENARIO] A sales credit memo without an applied invoice does not export an incomplete billing reference Initialize(); @@ -613,7 +624,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; CommentText: Text[80]; begin - // [FEATURE] [AI test 0.4] + // [FEATURE] [AI test] // [SCENARIO] Export includes an explicit regulatory note without synthesizing PMT, PMD, or AAB notes Initialize(); @@ -650,13 +661,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceLine: Record "Sales Invoice Line"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] An invoice containing lines from distinct orders uses the Extended CTC profile Initialize(); + // [GIVEN] Posted sales invoice "SI" with lines from distinct orders SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleOrders(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] Sales invoice "SI" is exported in PEPPOL BIS 3.0 FR ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] CustomizationID = 'EXTENDED-CTC-FR' Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); @@ -678,13 +693,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesShipmentHeader: Record "Sales Shipment Header"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] An invoice containing lines from distinct shipments uses the Extended CTC profile Initialize(); + // [GIVEN] Posted sales invoice "SI" with lines from distinct shipments SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromMultipleShipments(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] Sales invoice "SI" is exported in PEPPOL BIS 3.0 FR ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] CustomizationID = 'EXTENDED-CTC-FR' Assert.AreEqual('EXTENDED-CTC-FR', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); @@ -703,13 +722,17 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] Repeated references to one shipment and one order do not select the Extended CTC profile Initialize(); + // [GIVEN] Posted sales invoice "SI" with repeated references to one shipment and one order SalesInvoiceHeader.Get(CreateAndPostSalesInvoiceFromSingleShipment(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] Sales invoice "SI" is exported in PEPPOL BIS 3.0 FR ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] CustomizationID retains basic CTC profile Assert.AreEqual('urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', GetNodeByPath(XmlDoc, '/Invoice/cbc:CustomizationID'), StrSubstNo(IncorrectValueErr, 'CustomizationID')); end; @@ -753,6 +776,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); // [WHEN] Check is called + // [THEN] No error is raised CheckInvoice(SalesInvoiceHeader); // Cleanup @@ -769,6 +793,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" OriginalRegistrationNo: Text[20]; OriginalSIRETNo: Code[14]; begin + // [FEATURE] [AI test] // [SCENARIO] Company VAT registration number is used as the seller endpoint when SIRET and SIREN are blank Initialize(); @@ -807,6 +832,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CountryRegion: Record "Country/Region"; SalesInvoiceHeader: Record "Sales Invoice Header"; begin + // [FEATURE] [AI test] // [SCENARIO] A non-French company VAT registration number cannot be used as a French seller endpoint Initialize(); @@ -837,9 +863,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; EndpointId: Text[200]; begin + // [FEATURE] [AI test] // [SCENARIO] A service-specific company participant overrides the company endpoint fallbacks Initialize(); + // [GIVEN] Company with service participant using scheme 0002 EndpointId := CompanyInformation."Registration No."; ServiceParticipant.Service := EDocumentService.Code; ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; @@ -857,9 +885,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" CompanyInformation."Registration No." := ''; CompanyInformation.Modify(true); + // [WHEN] Sales invoice is checked and exported CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] Seller EndpointID uses service participant value Assert.AreEqual(EndpointId, GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); @@ -905,6 +935,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; XmlDoc: XmlDocument; begin + // [FEATURE] [AI test] // [SCENARIO] Customer Registration Number is used when both FR Electronic Address and Service Participant are absent Initialize(); @@ -996,9 +1027,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Customer: Record Customer; SalesInvoiceHeader: Record "Sales Invoice Header"; begin + // [FEATURE] [AI test] // [SCENARIO] A malformed FR Electronic Address that does not match SIREN format is rejected Initialize(); + // [GIVEN] Customer "C" with malformed FR Electronic Address and no Registration Number Customer.Get(CreateCustomer('ABCD56789', "Electronic Address Scheme"::"0002")); Customer."Registration Number" := ''; Customer.Modify(true); @@ -1019,17 +1052,21 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" XmlDoc: XmlDocument; OriginalSIRETNo: Code[14]; begin + // [FEATURE] [AI test] // [SCENARIO] Company Registration No. is used as the seller endpoint when SIRET is missing Initialize(); + // [GIVEN] Company with blank SIRET No. OriginalSIRETNo := CompanyInformation."SIRET No."; CompanyInformation."SIRET No." := ''; CompanyInformation.Modify(true); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] Sales invoice is checked and exported CheckInvoice(SalesInvoiceHeader); ExportInvoice(SalesInvoiceHeader, XmlDoc); + // [THEN] Supplier EndpointID uses Registration No. with scheme 0002 Assert.AreEqual(CompanyInformation."Registration No.", GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), StrSubstNo(IncorrectValueErr, 'Seller EndpointID')); @@ -1037,6 +1074,7 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" GetNodeByPath(XmlDoc, '/Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID'), StrSubstNo(IncorrectValueErr, 'Seller EndpointID schemeID')); + // Cleanup CompanyInformation.Get(); CompanyInformation."SIRET No." := OriginalSIRETNo; CompanyInformation.Modify(true); @@ -1048,9 +1086,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant: Record "Service Participant"; SalesInvoiceHeader: Record "Sales Invoice Header"; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a company participant identifier without its scheme even when SIRET is valid Initialize(); + // [GIVEN] Company with service participant missing FR Identifier Scheme ServiceParticipant.Init(); ServiceParticipant.Service := EDocumentService.Code; ServiceParticipant."Participant Type" := ServiceParticipant."Participant Type"::Company; @@ -1058,8 +1098,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CreateCustomer('123456789', "Electronic Address Scheme"::"0002"))); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about incomplete service participant is raised AssertExpectedDialogError(EDocHelpers.GetServiceParticipantAddressIncompleteError()); end; @@ -1069,16 +1111,20 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a buyer without an electronic address, Registration Number, or a service participant identifier Initialize(); + // [GIVEN] Customer "C" without electronic address, Registration Number, or VAT CustomerNo := CreateCustomer('', "Electronic Address Scheme"::"EM"); ClearCustomerVATRegistrationNo(CustomerNo); ClearCustomerRegistrationNumber(CustomerNo); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about buyer electronic address is raised; AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressRequiredError(CustomerNo)); end; @@ -1089,17 +1135,21 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Customer: Record Customer; CustomerNo: Code[20]; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a buyer electronic address that does not match SIREN or SIREN_suffix format Initialize(); + // [GIVEN] Customer "C" with short malformed FR Electronic Address CustomerNo := CreateCustomer('SHORT', "Electronic Address Scheme"::"0002"); Customer.Get(CustomerNo); Customer."Registration Number" := ''; Customer.Modify(true); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about malformed buyer electronic address is raised AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressInvalidError( Customer.FieldCaption("FR Electronic Address"), CustomerNo)); end; @@ -1111,17 +1161,21 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" Customer: Record Customer; CustomerNo: Code[20]; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a buyer electronic address with a blank SIREN suffix Initialize(); + // [GIVEN] Customer "C" with FR Electronic Address having a blank suffix CustomerNo := CreateCustomer('123456789_ ', "Electronic Address Scheme"::"0225"); Customer.Get(CustomerNo); Customer."Registration Number" := ''; Customer.Modify(true); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about malformed buyer electronic address is raised AssertExpectedDialogError(EDocHelpers.GetBuyerElectronicAddressInvalidError( Customer.FieldCaption("FR Electronic Address"), CustomerNo)); end; @@ -1133,9 +1187,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a service participant identifier without its French identifier scheme even when the customer endpoint is valid Initialize(); + // [GIVEN] Customer "C" with service participant missing FR Identifier Scheme CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); ServiceParticipant.Init(); ServiceParticipant.Service := EDocumentService.Code; @@ -1145,8 +1201,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about incomplete service participant is raised; AssertExpectedDialogError(EDocHelpers.GetServiceParticipantAddressIncompleteError()); end; @@ -1157,9 +1215,11 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" SalesInvoiceHeader: Record "Sales Invoice Header"; CustomerNo: Code[20]; begin + // [FEATURE] [AI test] // [SCENARIO] Check rejects a French identifier scheme without its service participant identifier even when the customer endpoint is valid Initialize(); + // [GIVEN] Customer "C" with FR Identifier Scheme but missing Participant Identifier CustomerNo := CreateCustomer('buyer@example.com', "Electronic Address Scheme"::"EM"); ServiceParticipant.Init(); ServiceParticipant.Service := EDocumentService.Code; @@ -1169,8 +1229,10 @@ codeunit 148147 "PEPPOL BIS 3.0 XML Tests" ServiceParticipant.Insert(); SalesInvoiceHeader.Get(CreateAndPostSalesInvoice(CustomerNo)); + // [WHEN] Check is called asserterror CheckInvoice(SalesInvoiceHeader); + // [THEN] Error about incomplete service participant is raised; AssertExpectedDialogError(EDocHelpers.GetServiceParticipantAddressIncompleteError()); end; #endregion From 2e6e4e386912b6e9047ec788af05fbf0e6732ecd Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 24 Aug 2026 23:10:28 +0200 Subject: [PATCH 21/49] Object ID changed --- .../Test/src/Processing/EDocMessageMgtTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 773f1ed74fd..0f0cb1081bf 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -11,7 +11,7 @@ using Microsoft.Sales.Customer; using System.Threading; using System.Utilities; -codeunit 139899 "E-Doc. Message Mgt. Tests" +codeunit 139893 "E-Doc. Message Mgt. Tests" { Subtype = Test; TestType = IntegrationTest; From e3a91c8aed067a08f4b0cffa64442bf4366e0f1d Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 25 Aug 2026 10:05:36 +0200 Subject: [PATCH 22/49] Fix E-Document test object ID collision --- .../Test/src/Processing/EDocMessageResponseTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al index 81257eff3dc..eeac498fa78 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al @@ -11,7 +11,7 @@ using Microsoft.Peppol.Response; using Microsoft.Sales.Customer; using System.Utilities; -codeunit 139898 "E-Doc. Message Response Tests" +codeunit 139864 "E-Doc. Message Response Tests" { Subtype = Test; TestType = IntegrationTest; From b7a91c6b92c3d6362d75bb2827510ce7ce9e9d59 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 25 Aug 2026 23:42:01 +0200 Subject: [PATCH 23/49] update --- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index b6723a5185e..148f01c67bf 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -56,7 +56,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." OriginalOccurrence: Record "E-Doc. Payment Occurrence"; begin EDocument.Get(EDocPaymentOccurrence."E-Document Entry No."); - if not IsEligibleFrenchEDocument(EDocument) then + if not ResolveEligibleFrenchService(EDocument) then exit; if EDocPaymentOccurrence.Type = EDocPaymentOccurrence.Type::Applied then begin @@ -380,18 +380,37 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(GeneralLedgerSetup."LCY Code"); end; - local procedure IsEligibleFrenchEDocument(EDocument: Record "E-Document"): Boolean + local procedure ResolveEligibleFrenchService(var EDocument: Record "E-Document"): Boolean var EDocumentService: Record "E-Document Service"; EDocumentServiceStatus: Record "E-Document Service Status"; begin - if not EDocumentService.Get(EDocument.Service) then - exit(false); - if not (EDocumentService."Document Format" in [EDocumentService."Document Format"::"Peppol BIS 3.0 FR", EDocumentService."Document Format"::"Factur-X FR"]) then - exit(false); - if not EDocumentServiceStatus.Get(EDocument."Entry No", EDocument.Service) then + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + EDocumentServiceStatus.SetFilter(Status, '%1|%2', EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared); + if EDocument.Service <> '' then begin + EDocumentServiceStatus.SetRange("E-Document Service Code", EDocument.Service); + if EDocumentServiceStatus.FindFirst() then + if IsSupportedFrenchService(EDocumentServiceStatus."E-Document Service Code", EDocumentService) then + exit(true); + EDocumentServiceStatus.SetRange("E-Document Service Code"); + end; + + if EDocumentServiceStatus.FindSet() then + repeat + if IsSupportedFrenchService(EDocumentServiceStatus."E-Document Service Code", EDocumentService) then begin + EDocument.Service := EDocumentService.Code; + exit(true); + end; + until EDocumentServiceStatus.Next() = 0; + + exit(false); + end; + + local procedure IsSupportedFrenchService(ServiceCode: Code[20]; var EDocumentService: Record "E-Document Service"): Boolean + begin + if not EDocumentService.Get(ServiceCode) then exit(false); - exit(EDocumentServiceStatus.Status in [EDocumentServiceStatus.Status::Approved, EDocumentServiceStatus.Status::Cleared]); + exit(EDocumentService."Document Format" in [EDocumentService."Document Format"::"Peppol BIS 3.0 FR", EDocumentService."Document Format"::"Factur-X FR"]); end; local procedure IsCollectedReportingRequired(EDocument: Record "E-Document"; DetailedLedgerEntryNo: Integer): Boolean From 991a431e2c2e2a6ac5cc9c087aa67333b245e33d Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Fri, 28 Aug 2026 18:08:36 +0200 Subject: [PATCH 24/49] Fix Factur-X test variable shadowing --- .../EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index 241c18fc2ee..c3ef43209b5 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -2181,8 +2181,6 @@ codeunit 148148 "Factur-X CII XML Tests" end; local procedure CheckFacturX(var SourceDocumentHeader: RecordRef) - var - EDocumentService: Record "E-Document Service"; begin FacturXFormat.Check(SourceDocumentHeader, EDocumentService, "E-Document Processing Phase"::Create); end; From 24e1a2d8d49724626348a1a260edb6d7c6b6e94b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 30 Aug 2026 12:11:30 +0200 Subject: [PATCH 25/49] Added payload and external-reference cleanup --- .../EReportingFRUser.PermissionSet.al | 2 +- .../app/src/Core/FREInvoiceMessage.Table.al | 1 + .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 1 + .../src/Core/FREInvoiceMessageVAT.Table.al | 1 + .../app/src/Core/FREInvoiceMessages.Page.al | 78 ++++++++++++++++ .../EReporting/EReportingFormat.Codeunit.al | 6 +- .../src/ExportEReportingTests.Codeunit.al | 44 +++++++++ .../App/src/Document/EDocument.Table.al | 5 ++ .../Integration/ServiceIntegration.Enum.al | 5 +- .../EDocumentBackgroundJobs.Codeunit.al | 2 +- .../Message/EDocMessageMgt.Codeunit.al | 50 +++++++++-- .../EDocMessageResponseJob.Codeunit.al | 9 +- .../Message/EDocMessageSendJob.Codeunit.al | 9 +- .../EDocMsgTransportDefault.Codeunit.al | 2 + .../Message/EDocumentMessage.Table.al | 11 +++ .../Test/src/Mock/EDocImplState.Codeunit.al | 24 ++--- .../Mock/EDocIntegrationMockV2.Codeunit.al | 15 +++- .../src/Mock/EDocIntegrationMockV2.EnumExt.al | 2 +- .../EDocMessageMgtTests.Codeunit.al | 89 +++++++++++++++++++ 19 files changed, 323 insertions(+), 33 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al index bb817cedc60..b77e42cd799 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.al @@ -15,7 +15,7 @@ permissionset 10988 "E-Reporting FR User" Permissions = table "FR E-Invoice Message" = X, - tabledata "FR E-Invoice Message" = RIMD, + tabledata "FR E-Invoice Message" = R, table "FR E-Invoice Message VAT" = X, tabledata "FR E-Invoice Message VAT" = R, codeunit "FR E-Invoice Message Mgt." = X, diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index b1538b6d033..92e166f5689 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -8,6 +8,7 @@ using Microsoft.eServices.EDocument; table 10970 "FR E-Invoice Message" { + Access = Internal; Caption = 'FR E-Invoice Message'; DataClassification = CustomerContent; InherentEntitlements = X; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 148f01c67bf..c76ee5ab561 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -249,6 +249,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." else VATEntryCurrencyErrorInfo.ErrorType(ErrorType::Internal); VATEntryCurrencyErrorInfo.Message(StrSubstNo(VATEntryCurrencyErr, VATEntry."Entry No.", CurrencyCode)); + VATEntryCurrencyErrorInfo.DataClassification := DataClassification::SystemMetadata; Error(VATEntryCurrencyErrorInfo); end; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al index 4b6fcb4336e..fcd0f085ec1 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al @@ -6,6 +6,7 @@ namespace Microsoft.eServices.EDocument.Formats; table 10971 "FR E-Invoice Message VAT" { + Access = Internal; Caption = 'FR E-Invoice Message VAT'; DataClassification = CustomerContent; InherentEntitlements = X; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index d02103e2586..d18d238773c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -4,6 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; + page 10973 "FR E-Invoice Messages" { ApplicationArea = Basic, Suite; @@ -68,6 +71,24 @@ page 10973 "FR E-Invoice Messages" ApplicationArea = Basic, Suite; ToolTip = 'Specifies the related generic E-Document message entry.'; } + field(MessageStatus; MessageStatus) + { + ApplicationArea = Basic, Suite; + Caption = 'Processing Status'; + ToolTip = 'Specifies the processing status of the related E-Document message.'; + } + field(MessageRetryCount; MessageRetryCount) + { + ApplicationArea = Basic, Suite; + Caption = 'Retry Count'; + ToolTip = 'Specifies how many background processing attempts have failed for the related E-Document message.'; + } + field(MessageLastError; MessageLastError) + { + ApplicationArea = Basic, Suite; + Caption = 'Last Error'; + ToolTip = 'Specifies the error returned by the most recent failed processing attempt.'; + } field("External Message ID"; Rec."External Message ID") { ApplicationArea = Basic, Suite; @@ -127,4 +148,61 @@ page 10973 "FR E-Invoice Messages" } } + actions + { + area(processing) + { + action(Retry) + { + ApplicationArea = Basic, Suite; + Caption = 'Retry'; + Enabled = RetryEnabled; + Image = Refresh; + Scope = Repeater; + ToolTip = 'Retry the failed lifecycle message transmission or response polling operation using its existing payload.'; + + trigger OnAction() + var + EDocumentMessageAPI: Codeunit "E-Document Message API"; + begin + EDocumentMessageAPI.RetryMessage(Rec."E-Document Message Entry No."); + CurrPage.Update(false); + end; + } + } + } + + trigger OnAfterGetRecord() + begin + LoadMessageState(); + end; + + trigger OnAfterGetCurrRecord() + begin + LoadMessageState(); + end; + + var + MessageStatus: Enum "E-Doc. Message Status"; + MessageLastError: Text[2048]; + MessageRetryCount: Integer; + RetryEnabled: Boolean; + + local procedure LoadMessageState() + var + EDocumentMessage: Record "E-Document Message"; + begin + Clear(MessageStatus); + Clear(MessageLastError); + Clear(MessageRetryCount); + RetryEnabled := false; + if (Rec."E-Document Message Entry No." = 0) or not EDocumentMessage.Get(Rec."E-Document Message Entry No.") then + exit; + + MessageStatus := EDocumentMessage.Status; + MessageLastError := EDocumentMessage."Last Error"; + MessageRetryCount := EDocumentMessage."Retry Count"; + RetryEnabled := (EDocumentMessage.Direction = EDocumentMessage.Direction::Outgoing) and + (EDocumentMessage.Status in [EDocumentMessage.Status::Error, EDocumentMessage.Status::"Response Error"]); + end; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al index 0ca100bb306..15603272557 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/EReporting/EReportingFormat.Codeunit.al @@ -102,7 +102,11 @@ codeunit 10970 "E-Reporting Format" implements "E-Document" if not EDocumentService.Get(Rec.Service) then exit; - if EDocumentService."Document Format" <> EDocumentService."Document Format"::"E-Reporting FR" then + if not (EDocumentService."Document Format" in [ + EDocumentService."Document Format"::"E-Reporting FR", + EDocumentService."Document Format"::"Peppol BIS 3.0 FR", + EDocumentService."Document Format"::"Factur-X FR"]) + then exit; if not EDocumentServiceStatus.Get(Rec."Entry No", Rec.Service) then diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al index 2c062954f35..e88336f2c58 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/ExportEReportingTests.Codeunit.al @@ -702,6 +702,50 @@ codeunit 148145 "Export E-Reporting Tests" Assert.AreNotEqual(0DT, EDocument."Clearance Date", 'Clearance Date should be non-zero after Approved status'); end; + [Test] + procedure ApprovedPeppolFRStatusSetsClearanceDate() + var + EDocument: Record "E-Document"; + begin + // [FEATURE] [AI test] + // [SCENARIO 637593] Approved Peppol BIS 3.0 FR status sets Clearance Date + Initialize(); + + // [GIVEN] E-Document "ED" linked to a Peppol BIS 3.0 FR service with Approved status + CreateEDocumentService('PEPPOLFR-APPR', "E-Document Format"::"Peppol BIS 3.0 FR"); + CreateEDocumentWithService(EDocument, 'PEPPOLFR-APPR'); + CreateEDocumentServiceStatus(EDocument."Entry No", 'PEPPOLFR-APPR', "E-Document Service Status"::Approved); + + // [WHEN] E-Document "ED" is modified + EDocument.Modify(true); + + // [THEN] Clearance Date is set + EDocument.Get(EDocument."Entry No"); + Assert.AreNotEqual(0DT, EDocument."Clearance Date", 'Clearance Date should be non-zero after Approved Peppol BIS 3.0 FR status'); + end; + + [Test] + procedure ApprovedFacturXFRStatusSetsClearanceDate() + var + EDocument: Record "E-Document"; + begin + // [FEATURE] [AI test] + // [SCENARIO 637593] Approved Factur-X FR status sets Clearance Date + Initialize(); + + // [GIVEN] E-Document "ED" linked to a Factur-X FR service with Approved status + CreateEDocumentService('FACTURXFR-APPR', "E-Document Format"::"Factur-X FR"); + CreateEDocumentWithService(EDocument, 'FACTURXFR-APPR'); + CreateEDocumentServiceStatus(EDocument."Entry No", 'FACTURXFR-APPR', "E-Document Service Status"::Approved); + + // [WHEN] E-Document "ED" is modified + EDocument.Modify(true); + + // [THEN] Clearance Date is set + EDocument.Get(EDocument."Entry No"); + Assert.AreNotEqual(0DT, EDocument."Clearance Date", 'Clearance Date should be non-zero after Approved Factur-X FR status'); + end; + [Test] procedure ClearedStatusSetsClearanceDate() var diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al b/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al index a65781e1187..379e788f22b 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al @@ -429,6 +429,7 @@ table 6121 "E-Document" EDocumentIntegrationLog: Record "E-Document Integration Log"; EDocumentLog: Record "E-Document Log"; EDocImportedLine: Record "E-Doc. Imported Line"; + EDocExternalReference: Record "E-Doc. External Reference"; EDocumentMessage: Record "E-Document Message"; EDocumentServiceStatus: Record "E-Document Service Status"; #if not CLEAN27 @@ -471,6 +472,10 @@ table 6121 "E-Document" if not EDocumentMessage.IsEmpty() then EDocumentMessage.DeleteAll(true); + EDocExternalReference.SetRange("E-Document Entry No.", Rec."Entry No"); + if not EDocExternalReference.IsEmpty() then + EDocExternalReference.DeleteAll(true); + #if not CLEAN27 // Version 1 processing cleanup // Can be removed soon as version 1 is fully migrated to version 2 diff --git a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al index 041f0603521..0892af92e84 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Integration/ServiceIntegration.Enum.al @@ -15,7 +15,10 @@ enum 6151 "Service Integration" implements IDocumentSender, IDocumentReceiver, I DefaultImplementation = IConsentManager = "Consent Manager Default Impl.", IMessageSender = "E-Doc. Msg. Transport Default", IMessageResponseHandler = "E-Doc. Msg. Transport Default"; - UnknownValueImplementation = IMessageSender = "E-Doc. Msg. Transport Default", + UnknownValueImplementation = IDocumentSender = "E-Document No Integration", + IDocumentReceiver = "E-Document No Integration", + IConsentManager = "E-Document No Integration", + IMessageSender = "E-Doc. Msg. Transport Default", IMessageResponseHandler = "E-Doc. Msg. Transport Default"; value(0; "No Integration") diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index ca180305fb7..8d2f10057ed 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -183,7 +183,7 @@ codeunit 6133 "E-Document Background Jobs" TelemetryDimensions.Add('Record Id', Format(JobRecordId)); TelemetryDimensions.Add('User Session ID', Format(JobQueueEntry."User Session ID")); TelemetryDimensions.Add('Earliest Start Date/Time', Format(JobQueueEntry."Earliest Start Date/Time")); - Telemetry.LogMessage('0000LC6', EDocumentJobTelemetryLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); + Telemetry.LogMessage('0000LC6', EDocumentJobTelemetryLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, TelemetryDimensions); Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry); exit(JobQueueEntry.ID); end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index d845d47d6c0..0f884f28758 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -122,6 +122,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; + ConnectorErrorInfo: ErrorInfo; MessageSendingErrorInfo: ErrorInfo; begin EDocMessage.Get(MessageEntryNo); @@ -138,11 +139,21 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; - MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); + if not TrySendMessage(MessageSender, EDocument, EDocumentService, EDocMessageContext) then begin + ConnectorErrorInfo := GetLastErrorObject(); + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + Commit(); + Error(ConnectorErrorInfo); + end; if not (EDocMessageContext.Status().GetStatus() in ["E-Document Service Status"::Sent, "E-Document Service Status"::"Pending Response"]) then begin + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + Commit(); MessageSendingErrorInfo.ErrorType := ErrorType::Internal; MessageSendingErrorInfo.Message := StrSubstNo(MessageSendingErr, MessageEntryNo); MessageSendingErrorInfo.DetailedMessage := StrSubstNo(MessageSendingDetailedErr, EDocMessageContext.Status().GetStatus()); + MessageSendingErrorInfo.DataClassification := DataClassification::SystemMetadata; Error(MessageSendingErrorInfo); end; @@ -155,8 +166,10 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage."Last Attempt At" := CurrentDateTime(); Clear(EDocMessage."Last Error"); EDocMessage.Modify(); - if EDocMessage.Status = EDocMessage.Status::"Pending Response" then + if EDocMessage.Status = EDocMessage.Status::"Pending Response" then begin + Commit(); EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + end; end; procedure PollMessageResponse(MessageEntryNo: Integer) @@ -169,6 +182,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageResponseHandler: Interface IMessageResponseHandler; + ConnectorErrorInfo: ErrorInfo; ResponseReceived: Boolean; begin EDocMessage.Get(MessageEntryNo); @@ -180,15 +194,29 @@ codeunit 6433 "E-Doc. Message Mgt." GetMessageBlob(MessageEntryNo, TempBlob); EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageResponseHandler := EDocumentService."Service Integration V2"; - ResponseReceived := MessageResponseHandler.GetResponse(EDocument, EDocumentService, EDocMessageContext); + if not TryGetResponse(MessageResponseHandler, EDocument, EDocumentService, EDocMessageContext, ResponseReceived) then begin + ConnectorErrorInfo := GetLastErrorObject(); + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + Commit(); + Error(ConnectorErrorInfo); + end; if ResponseReceived then begin - if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::Sent then begin + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + Commit(); Error(MessageResponseStatusErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + end; EDocMessage.Status := EDocMessage.Status::Sent; end else begin - if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::"Pending Response" then + if EDocMessageContext.Status().GetStatus() <> "E-Document Service Status"::"Pending Response" then begin + EDocumentLog.InsertIntegrationLog( + EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); + Commit(); Error(MessageResponseStatusErr, MessageEntryNo, EDocMessageContext.Status().GetStatus()); + end; EDocMessage.Status := EDocMessage.Status::"Pending Response"; end; @@ -201,6 +229,18 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); end; + [TryFunction] + local procedure TrySendMessage(MessageSender: Interface IMessageSender; var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; EDocMessageContext: Codeunit "E-Doc. Message Context") + begin + MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext); + end; + + [TryFunction] + local procedure TryGetResponse(MessageResponseHandler: Interface IMessageResponseHandler; var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; EDocMessageContext: Codeunit "E-Doc. Message Context"; var ResponseReceived: Boolean) + begin + ResponseReceived := MessageResponseHandler.GetResponse(EDocument, EDocumentService, EDocMessageContext); + end; + procedure QueueMessage(MessageEntryNo: Integer) var EDocMessage: Record "E-Document Message"; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al index e0f235d863f..8524d04b874 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al @@ -16,13 +16,15 @@ codeunit 6539 "E-Doc. Message Response Job" trigger OnRun() var EDocumentMessage: Record "E-Document Message"; + LastErrorInfo: ErrorInfo; LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); if TryPollMessageResponse(EDocumentMessage."Entry No.") then exit; - LastErrorText := GetLastErrorText(); + LastErrorInfo := GetLastErrorObject(); + LastErrorText := LastErrorInfo.Message; EDocumentMessage.Get(EDocumentMessage."Entry No."); EDocumentMessage.Status := EDocumentMessage.Status::"Response Error"; EDocumentMessage."Last Attempt At" := CurrentDateTime(); @@ -30,7 +32,7 @@ codeunit 6539 "E-Doc. Message Response Job" EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); EDocumentMessage.Modify(); Commit(); - Error(MessageResponseFailedErr, EDocumentMessage."Entry No.", LastErrorText); + Error(LastErrorInfo); end; [TryFunction] @@ -40,7 +42,4 @@ codeunit 6539 "E-Doc. Message Response Job" begin EDocMessageMgt.PollMessageResponse(MessageEntryNo); end; - - var - MessageResponseFailedErr: Label 'The response for E-Document message %1 could not be retrieved. %2', Comment = '%1 = message entry number, %2 = connector error'; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al index 28f31c79d3d..2cc17c6de0b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -16,13 +16,15 @@ codeunit 6535 "E-Doc. Message Send Job" trigger OnRun() var EDocumentMessage: Record "E-Document Message"; + LastErrorInfo: ErrorInfo; LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); if Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) then exit; - LastErrorText := GetLastErrorText(); + LastErrorInfo := GetLastErrorObject(); + LastErrorText := LastErrorInfo.Message; EDocumentMessage.Get(EDocumentMessage."Entry No."); EDocumentMessage.Status := EDocumentMessage.Status::Error; EDocumentMessage."Last Attempt At" := CurrentDateTime(); @@ -30,9 +32,6 @@ codeunit 6535 "E-Doc. Message Send Job" EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); EDocumentMessage.Modify(); Commit(); - Error(MessageSendFailedErr, EDocumentMessage."Entry No.", LastErrorText); + Error(LastErrorInfo); end; - - var - MessageSendFailedErr: Label 'E-Document message %1 could not be sent. %2', Comment = '%1 = message entry number, %2 = connector error'; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al index a8f18bbfa9e..62ce5a77ff2 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al @@ -18,6 +18,7 @@ codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender, IMessag MessageTransportErrorInfo: ErrorInfo; begin MessageTransportErrorInfo.Message := StrSubstNo(MessageTransportNotSupportedErr, EDocumentService.Code); + MessageTransportErrorInfo.DataClassification := DataClassification::SystemMetadata; MessageTransportErrorInfo.RecordId := EDocumentService.RecordId; MessageTransportErrorInfo.PageNo := Page::"E-Document Service"; MessageTransportErrorInfo.AddNavigationAction(ShowEDocumentServiceLbl); @@ -29,6 +30,7 @@ codeunit 6534 "E-Doc. Msg. Transport Default" implements IMessageSender, IMessag MessageTransportErrorInfo: ErrorInfo; begin MessageTransportErrorInfo.Message := StrSubstNo(MessageResponseNotSupportedErr, EDocumentService.Code); + MessageTransportErrorInfo.DataClassification := DataClassification::SystemMetadata; MessageTransportErrorInfo.RecordId := EDocumentService.RecordId; MessageTransportErrorInfo.PageNo := Page::"E-Document Service"; MessageTransportErrorInfo.AddNavigationAction(ShowEDocumentServiceLbl); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al index e8381145d14..8c7ff8d8537 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al @@ -114,4 +114,15 @@ table 6432 "E-Document Message" { } } + + trigger OnDelete() + var + EDocDataStorage: Record "E-Doc. Data Storage"; + begin + if Rec."Data Storage Entry No." = 0 then + exit; + + if EDocDataStorage.Get(Rec."Data Storage Entry No.") then + EDocDataStorage.Delete(true); + end; } diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al index 32c84962dae..ea68ee954f3 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al @@ -184,12 +184,6 @@ codeunit 139630 "E-Doc. Impl. State" IsAsync := IsAsync2; HttpResponse := LocalHttpResponse; - if ThrowIntegrationRuntimeError then - Error('TEST'); - - if ThrowIntegrationLoggedError then - EDocErrorHelper.LogSimpleErrorMessage(EDocument, 'TEST'); - if EnableHttpData then begin HttpRequest.SetRequestUri('http://cronus.test'); HttpRequest.Method := 'POST'; @@ -199,6 +193,12 @@ codeunit 139630 "E-Doc. Impl. State" HttpResponse.Headers.Add('Accept', '*'); end; + if ThrowIntegrationRuntimeError then + Error('TEST'); + + if ThrowIntegrationLoggedError then + EDocErrorHelper.LogSimpleErrorMessage(EDocument, 'TEST'); + end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Int Mock No Async", OnSend, '', false, false)] @@ -233,12 +233,6 @@ codeunit 139630 "E-Doc. Impl. State" Success := OnGetResponseSuccess; HttpResponse := LocalHttpResponse; - if ThrowIntegrationRuntimeError then - Error('TEST'); - - if ThrowIntegrationLoggedError then - EDocErrorHelper.LogSimpleErrorMessage(EDocument, 'TEST'); - if EnableHttpData then begin HttpRequest.SetRequestUri('http://cronus.test'); HttpRequest.Method := 'POST'; @@ -247,6 +241,12 @@ codeunit 139630 "E-Doc. Impl. State" HttpResponse.Content.WriteFrom('Test response'); HttpResponse.Headers.Add('Accept', '*'); end; + + if ThrowIntegrationRuntimeError then + Error('TEST'); + + if ThrowIntegrationLoggedError then + EDocErrorHelper.LogSimpleErrorMessage(EDocument, 'TEST'); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Integration Mock V2", OnReceiveDocuments, '', false, false)] diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al index 0b1706f65bf..7694ff5608f 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.Codeunit.al @@ -11,7 +11,7 @@ using Microsoft.eServices.EDocument.Integration.Send; using Microsoft.eServices.EDocument.Processing.Message; using System.Utilities; -codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager, IMessageResponseHandler +codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager, IMessageSender, IMessageResponseHandler { Access = Internal; @@ -50,6 +50,19 @@ codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocume exit(ResponseReceived); end; + procedure SendMessage(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context") + var + TempBlob: Codeunit "Temp Blob"; + IsAsync: Boolean; + begin + TempBlob := MessageContext.GetTempBlob(); + OnSend(EDocument, EDocumentService, TempBlob, IsAsync, MessageContext.Http().GetHttpRequestMessage(), MessageContext.Http().GetHttpResponseMessage()); + if IsAsync then + MessageContext.Status().SetStatus("E-Document Service Status"::"Pending Response") + else + MessageContext.Status().SetStatus("E-Document Service Status"::Sent); + end; + procedure ReceiveDocuments(var EDocumentService: Record "E-Document Service"; DocumentsMetadata: Codeunit "Temp Blob List"; ReceiveContext: Codeunit ReceiveContext) begin OnReceiveDocuments(DocumentsMetadata, ReceiveContext.Http().GetHttpRequestMessage(), ReceiveContext.Http().GetHttpResponseMessage()); diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al index d68f40bc8cd..9e60a751268 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocIntegrationMockV2.EnumExt.al @@ -12,7 +12,7 @@ enumextension 139617 "E-Doc Integration Mock V2" extends "Service Integration" value(133501; "Mock") { - Implementation = IDocumentSender = "E-Doc. Integration Mock V2", IDocumentReceiver = "E-Doc. Integration Mock V2", IConsentManager = "E-Doc. Integration Mock V2", IMessageResponseHandler = "E-Doc. Integration Mock V2"; + Implementation = IDocumentSender = "E-Doc. Integration Mock V2", IDocumentReceiver = "E-Doc. Integration Mock V2", IConsentManager = "E-Doc. Integration Mock V2", IMessageSender = "E-Doc. Integration Mock V2", IMessageResponseHandler = "E-Doc. Integration Mock V2"; } value(133502; "Mock Sync") { diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 0f0cb1081bf..14d8837d2a2 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -170,6 +170,46 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" Assert.ExpectedError('Direction must have the value Outgoing'); end; + [Test] + procedure CleanupDocumentDeletesMessagePayloadAndExternalReference() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocDataStorage: Record "E-Doc. Data Storage"; + EDocExternalReference: Record "E-Doc. External Reference"; + EDocMessage: Record "E-Document Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + DataStorageEntryNo: Integer; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO 637593] Cleaning an E-Document removes child message storage and external correlation + Initialize(Customer); + + // [GIVEN] E-Document "ED" with a child message payload and an external document reference + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + EDocMessage.Get(MessageEntryNo); + DataStorageEntryNo := EDocMessage."Data Storage Entry No."; + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, 'EXTERNAL-DOCUMENT-ID'); + + // [WHEN] E-Document "ED" is cleaned up + EDocument.CleanupDocument(); + + // [THEN] The child message, its payload, and its external reference are deleted + Assert.IsFalse(EDocMessage.Get(MessageEntryNo), 'The child message must be deleted.'); + Assert.IsFalse(EDocDataStorage.Get(DataStorageEntryNo), 'The child message payload must be deleted.'); + EDocExternalReference.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.RecordIsEmpty(EDocExternalReference); + end; + [Test] procedure PollMessageResponseCompletesPendingMessage() var @@ -236,6 +276,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" var Customer: Record Customer; EDocument: Record "E-Document"; + EDocumentIntegrationLog: Record "E-Document Integration Log"; EDocMessage: Record "E-Document Message"; JobQueueEntry: Record "Job Queue Entry"; MessageEntryNo: Integer; @@ -250,6 +291,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" EDocMessage.Get(MessageEntryNo); JobQueueEntry."Record ID to Process" := EDocMessage.RecordId(); BindSubscription(EDocImplState); + EDocImplState.SetEnableHttpData(); EDocImplState.SetThrowIntegrationRuntimeError(); // [WHEN] The response polling background job runs @@ -261,6 +303,53 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" Assert.AreEqual(EDocMessage.Status::"Response Error", EDocMessage.Status, 'The message must have a response error.'); Assert.AreEqual(1, EDocMessage."Retry Count", 'The failed polling attempt must increment the retry count.'); Assert.IsTrue(EDocMessage."Last Error".Contains('TEST'), 'The connector error must be stored.'); + EDocumentIntegrationLog.SetRange("E-Doc. Entry No", EDocument."Entry No"); + Assert.RecordCount(EDocumentIntegrationLog, 1); + end; + + [Test] + procedure SendMessageJobStoresConnectorErrorAndIntegrationLog() + var + Customer: Record Customer; + EDocument: Record "E-Document"; + EDocumentIntegrationLog: Record "E-Document Integration Log"; + EDocMessage: Record "E-Document Message"; + JobQueueEntry: Record "Job Queue Entry"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + OutStream: OutStream; + MessageEntryNo: Integer; + begin + // [FEATURE] [AI test] + // [SCENARIO 647423] A failed child-message send retains diagnostics and the HTTP exchange + Initialize(Customer); + + // [GIVEN] Queued message "M" and a connector that records HTTP data before failing + CreateOutgoingEDocument(EDocument); + TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); + OutStream.WriteText(''); + MessageEntryNo := EDocMessageMgt.CreateMessage( + EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, + "E-Doc. Response Type"::None, TempBlob); + EDocMessage.Get(MessageEntryNo); + EDocMessage.Status := EDocMessage.Status::Queued; + EDocMessage.Modify(); + JobQueueEntry."Record ID to Process" := EDocMessage.RecordId(); + BindSubscription(EDocImplState); + EDocImplState.SetEnableHttpData(); + EDocImplState.SetThrowIntegrationRuntimeError(); + + // [WHEN] The message send background job runs + Assert.IsFalse(Codeunit.Run(Codeunit::"E-Doc. Message Send Job", JobQueueEntry), 'The send job must report the connector failure.'); + UnbindSubscription(EDocImplState); + + // [THEN] Message "M" contains diagnostics and its HTTP exchange is retained + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual(EDocMessage.Status::Error, EDocMessage.Status, 'The message must have a send error.'); + Assert.AreEqual(1, EDocMessage."Retry Count", 'The failed send attempt must increment the retry count.'); + Assert.IsTrue(EDocMessage."Last Error".Contains('TEST'), 'The connector error must be stored.'); + EDocumentIntegrationLog.SetRange("E-Doc. Entry No", EDocument."Entry No"); + Assert.RecordCount(EDocumentIntegrationLog, 1); end; [Test] From 03527079214902aba1ba535ebc608eee4c62c587 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 30 Aug 2026 19:40:20 +0200 Subject: [PATCH 26/49] removed msg status and retry --- .../app/src/Core/FREInvoiceMessages.Page.al | 79 ------------------- 1 file changed, 79 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index d18d238773c..37693f2bbbf 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -4,9 +4,6 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; -using Microsoft.eServices.EDocument; -using Microsoft.eServices.EDocument.Processing.Message; - page 10973 "FR E-Invoice Messages" { ApplicationArea = Basic, Suite; @@ -71,24 +68,6 @@ page 10973 "FR E-Invoice Messages" ApplicationArea = Basic, Suite; ToolTip = 'Specifies the related generic E-Document message entry.'; } - field(MessageStatus; MessageStatus) - { - ApplicationArea = Basic, Suite; - Caption = 'Processing Status'; - ToolTip = 'Specifies the processing status of the related E-Document message.'; - } - field(MessageRetryCount; MessageRetryCount) - { - ApplicationArea = Basic, Suite; - Caption = 'Retry Count'; - ToolTip = 'Specifies how many background processing attempts have failed for the related E-Document message.'; - } - field(MessageLastError; MessageLastError) - { - ApplicationArea = Basic, Suite; - Caption = 'Last Error'; - ToolTip = 'Specifies the error returned by the most recent failed processing attempt.'; - } field("External Message ID"; Rec."External Message ID") { ApplicationArea = Basic, Suite; @@ -147,62 +126,4 @@ page 10973 "FR E-Invoice Messages" } } } - - actions - { - area(processing) - { - action(Retry) - { - ApplicationArea = Basic, Suite; - Caption = 'Retry'; - Enabled = RetryEnabled; - Image = Refresh; - Scope = Repeater; - ToolTip = 'Retry the failed lifecycle message transmission or response polling operation using its existing payload.'; - - trigger OnAction() - var - EDocumentMessageAPI: Codeunit "E-Document Message API"; - begin - EDocumentMessageAPI.RetryMessage(Rec."E-Document Message Entry No."); - CurrPage.Update(false); - end; - } - } - } - - trigger OnAfterGetRecord() - begin - LoadMessageState(); - end; - - trigger OnAfterGetCurrRecord() - begin - LoadMessageState(); - end; - - var - MessageStatus: Enum "E-Doc. Message Status"; - MessageLastError: Text[2048]; - MessageRetryCount: Integer; - RetryEnabled: Boolean; - - local procedure LoadMessageState() - var - EDocumentMessage: Record "E-Document Message"; - begin - Clear(MessageStatus); - Clear(MessageLastError); - Clear(MessageRetryCount); - RetryEnabled := false; - if (Rec."E-Document Message Entry No." = 0) or not EDocumentMessage.Get(Rec."E-Document Message Entry No.") then - exit; - - MessageStatus := EDocumentMessage.Status; - MessageLastError := EDocumentMessage."Last Error"; - MessageRetryCount := EDocumentMessage."Retry Count"; - RetryEnabled := (EDocumentMessage.Direction = EDocumentMessage.Direction::Outgoing) and - (EDocumentMessage.Status in [EDocumentMessage.Status::Error, EDocumentMessage.Status::"Response Error"]); - end; } \ No newline at end of file From 7482b3e8f400792f49a6ec9c1f2c926888d43279 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 30 Aug 2026 22:38:03 +0200 Subject: [PATCH 27/49] upd --- .../Message/EDocPaymentOccurrenceMgt.Codeunit.al | 1 - .../src/Processing/Message/EDocumentMessage.Table.al | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 5be2d3c7ffc..61e93915b3c 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -127,7 +127,6 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." if not SalesInvoiceHeader.Get(InvoiceCustLedgerEntry."Document No.") then exit(false); - EDocument.SetLoadFields("Entry No"); EDocument.SetRange("Document Record ID", SalesInvoiceHeader.RecordId); EDocument.SetRange(Direction, EDocument.Direction::Outgoing); EDocument.SetRange("Document Type", EDocument."Document Type"::"Sales Invoice"); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al index 8c7ff8d8537..e8381145d14 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al @@ -114,15 +114,4 @@ table 6432 "E-Document Message" { } } - - trigger OnDelete() - var - EDocDataStorage: Record "E-Doc. Data Storage"; - begin - if Rec."Data Storage Entry No." = 0 then - exit; - - if EDocDataStorage.Get(Rec."Data Storage Entry No.") then - EDocDataStorage.Delete(true); - end; } From 492e6bc0eaf01281941b5e313b72bc56786e5775 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sun, 30 Aug 2026 23:07:39 +0200 Subject: [PATCH 28/49] move tooltips --- .../app/src/Core/FREInvoiceMessage.Table.al | 20 +++++++++++++++++++ .../app/src/Core/FREInvoiceMessages.Page.al | 20 ------------------- .../test/src/FacturXCIIXMLTests.Codeunit.al | 11 ++-------- 3 files changed, 22 insertions(+), 29 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 92e166f5689..2b0a8ff7c27 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -33,17 +33,20 @@ table 10970 "FR E-Invoice Message" { Caption = 'Type'; DataClassification = SystemMetadata; + ToolTip = 'Specifies the French lifecycle status represented by this message.'; } field(4; "Source Occurrence ID"; Guid) { Caption = 'Source Occurrence ID'; DataClassification = SystemMetadata; + ToolTip = 'Specifies the immutable source identifier used to prevent duplicate lifecycle messages.'; } field(5; "Original Entry No."; Integer) { Caption = 'Original Entry No.'; DataClassification = SystemMetadata; TableRelation = "FR E-Invoice Message"."Entry No."; + ToolTip = 'Specifies the original collected message reversed by a negative collected message.'; } field(6; Amount; Decimal) { @@ -51,16 +54,19 @@ table 10970 "FR E-Invoice Message" AutoFormatType = 1; Caption = 'Amount'; DataClassification = CustomerContent; + ToolTip = 'Specifies the payment amount reported by a collected or negative collected message.'; } field(7; "Currency Code"; Code[10]) { Caption = 'Currency Code'; DataClassification = CustomerContent; + ToolTip = 'Specifies the currency of the reported payment amount.'; } field(8; "Event Date"; Date) { Caption = 'Event Date'; DataClassification = CustomerContent; + ToolTip = 'Specifies the business date on which the lifecycle event occurred.'; } field(9; "Detailed Ledger Entry No."; Integer) { @@ -71,71 +77,85 @@ table 10970 "FR E-Invoice Message" { Caption = 'Reason Code'; DataClassification = CustomerContent; + ToolTip = 'Specifies the reason code supplied for the lifecycle status.'; } field(11; "Reason Description"; Text[500]) { Caption = 'Reason Description'; DataClassification = CustomerContent; + ToolTip = 'Specifies the reason description supplied for the lifecycle status.'; } field(12; "E-Document Message Entry No."; Integer) { Caption = 'E-Document Message Entry No.'; DataClassification = SystemMetadata; + ToolTip = 'Specifies the related generic E-Document message entry.'; } field(13; "Created At"; DateTime) { Caption = 'Created At'; DataClassification = SystemMetadata; + ToolTip = 'Specifies when the French lifecycle message was created.'; } field(14; "External Message ID"; Text[250]) { Caption = 'External Message ID'; DataClassification = CustomerContent; + ToolTip = 'Specifies the identifier assigned to an incoming lifecycle message by the external service.'; } field(15; "Received At"; DateTime) { Caption = 'Received At'; DataClassification = SystemMetadata; + ToolTip = 'Specifies when the incoming lifecycle message was received.'; } field(16; "Sender Platform ID"; Text[50]) { Caption = 'Sender Platform ID'; DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the frozen identifier of the sender platform.'; } field(17; "Sender Platform Scheme"; Code[4]) { Caption = 'Sender Platform Scheme'; DataClassification = SystemMetadata; + ToolTip = 'Specifies the frozen identifier scheme of the sender platform.'; } field(18; "Sender Platform Name"; Text[100]) { Caption = 'Sender Platform Name'; DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the frozen name of the sender platform.'; } field(19; "Invoice Issue Date"; Date) { Caption = 'Invoice Issue Date'; DataClassification = CustomerContent; + ToolTip = 'Specifies the frozen issue date of the invoice.'; } field(20; "Invoice Receipt At"; DateTime) { Caption = 'Invoice Receipt At'; DataClassification = CustomerContent; + ToolTip = 'Specifies the frozen date and time when the sender platform received the invoice.'; } field(21; "Invoice Issuer ID"; Text[50]) { Caption = 'Invoice Issuer ID'; DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the frozen SIREN identifier of the invoice issuer.'; } field(22; "Invoice Issuer Scheme"; Code[4]) { Caption = 'Invoice Issuer Scheme'; DataClassification = SystemMetadata; + ToolTip = 'Specifies the frozen identifier scheme of the invoice issuer.'; } field(23; "Invoice Issuer Name"; Text[100]) { Caption = 'Invoice Issuer Name'; DataClassification = OrganizationIdentifiableInformation; + ToolTip = 'Specifies the frozen name of the invoice issuer.'; } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al index 37693f2bbbf..65778eff0e5 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al @@ -26,102 +26,82 @@ page 10973 "FR E-Invoice Messages" field(Type; Rec.Type) { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the French lifecycle status represented by this message.'; } field(Amount; Rec.Amount) { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the payment amount reported by a collected or negative collected message.'; } field("Currency Code"; Rec."Currency Code") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the currency of the reported payment amount.'; } field("Event Date"; Rec."Event Date") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the business date on which the lifecycle event occurred.'; } field("Reason Code"; Rec."Reason Code") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the reason code supplied for the lifecycle status.'; } field("Reason Description"; Rec."Reason Description") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the reason description supplied for the lifecycle status.'; } field("Source Occurrence ID"; Rec."Source Occurrence ID") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the immutable source identifier used to prevent duplicate lifecycle messages.'; } field("Original Entry No."; Rec."Original Entry No.") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the original collected message reversed by a negative collected message.'; } field("E-Document Message Entry No."; Rec."E-Document Message Entry No.") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the related generic E-Document message entry.'; } field("External Message ID"; Rec."External Message ID") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the identifier assigned to an incoming lifecycle message by the external service.'; } field("Received At"; Rec."Received At") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies when the incoming lifecycle message was received.'; } field("Sender Platform ID"; Rec."Sender Platform ID") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen identifier of the sender platform.'; } field("Sender Platform Scheme"; Rec."Sender Platform Scheme") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen identifier scheme of the sender platform.'; } field("Sender Platform Name"; Rec."Sender Platform Name") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen name of the sender platform.'; } field("Invoice Issue Date"; Rec."Invoice Issue Date") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen issue date of the invoice.'; } field("Invoice Receipt At"; Rec."Invoice Receipt At") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen date and time when the sender platform received the invoice.'; } field("Invoice Issuer ID"; Rec."Invoice Issuer ID") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen SIREN identifier of the invoice issuer.'; } field("Invoice Issuer Scheme"; Rec."Invoice Issuer Scheme") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen identifier scheme of the invoice issuer.'; } field("Invoice Issuer Name"; Rec."Invoice Issuer Name") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies the frozen name of the invoice issuer.'; } field("Created At"; Rec."Created At") { ApplicationArea = Basic, Suite; - ToolTip = 'Specifies when the French lifecycle message was created.'; } } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al index c3ef43209b5..0ded9d84f00 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FacturXCIIXMLTests.Codeunit.al @@ -12,7 +12,6 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Address; using Microsoft.Foundation.Company; -using Microsoft.Foundation.Reporting; using Microsoft.Foundation.UOM; using Microsoft.Inventory.Item; using Microsoft.Inventory.Location; @@ -1221,7 +1220,6 @@ codeunit 148148 "Factur-X CII XML Tests" procedure FacturXSalesCrMemoZeroVATCatSPreservedWithGermanBuyer() var Customer: Record Customer; - DocumentSendingProfile: Record "Document Sending Profile"; GLAccount: Record "G/L Account"; VATPostingSetup: Record "VAT Posting Setup"; SalesHeader: Record "Sales Header"; @@ -1247,13 +1245,6 @@ codeunit 148148 "Factur-X CII XML Tests" Customer."VAT Registration No." := '533435789'; Customer."Registration Number" := ''; Customer."FR Electronic Address" := '123456789_FOREIGN'; - if not DocumentSendingProfile.Get('NON-EDOC') then begin - DocumentSendingProfile.Init(); - DocumentSendingProfile.Code := 'NON-EDOC'; - DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::No; - DocumentSendingProfile.Insert(true); - end; - Customer.Validate("Document Sending Profile", DocumentSendingProfile.Code); Customer.Modify(true); CustomerNo := Customer."No."; @@ -2181,6 +2172,8 @@ codeunit 148148 "Factur-X CII XML Tests" end; local procedure CheckFacturX(var SourceDocumentHeader: RecordRef) + var + EDocumentService: Record "E-Document Service"; begin FacturXFormat.Check(SourceDocumentHeader, EDocumentService, "E-Document Processing Phase"::Create); end; From c33852021fd0a63256cc87bcebc6f488be190f57 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 00:08:03 +0200 Subject: [PATCH 29/49] ResolveFrenchService condition --- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 25 +++++++ .../src/FREInvoiceMessageTests.Codeunit.al | 69 +++++++------------ .../Message/EDocMessageMgt.Codeunit.al | 12 ++-- 3 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index c76ee5ab561..21d06203820 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -24,12 +24,16 @@ codeunit 10975 "FR E-Invoice Message Mgt." internal procedure AcceptInvoice(EDocument: Record "E-Document") begin + if not ResolveFrenchService(EDocument) then + Error(FrenchServiceNotFoundErr, EDocument."Entry No"); CheckBuyerResponseAllowed(EDocument); CreateAndSendMessage(EDocument, "FR E-Invoice Message Type"::Accepted, CreateGuid(), 0, '', Today(), 0, 0, '', ''); end; internal procedure RefuseInvoice(EDocument: Record "E-Document"; ReasonCode: Code[20]; ReasonDescription: Text[500]) begin + if not ResolveFrenchService(EDocument) then + Error(FrenchServiceNotFoundErr, EDocument."Entry No"); CheckBuyerResponseAllowed(EDocument); CreateAndSendMessage(EDocument, "FR E-Invoice Message Type"::Refused, CreateGuid(), 0, '', Today(), 0, 0, ReasonCode, ReasonDescription); end; @@ -407,6 +411,26 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(false); end; + local procedure ResolveFrenchService(var EDocument: Record "E-Document"): Boolean + var + EDocumentService: Record "E-Document Service"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + if IsSupportedFrenchService(EDocument.Service, EDocumentService) then + exit(true); + + EDocumentServiceStatus.SetRange("E-Document Entry No", EDocument."Entry No"); + if EDocumentServiceStatus.FindSet() then + repeat + if IsSupportedFrenchService(EDocumentServiceStatus."E-Document Service Code", EDocumentService) then begin + EDocument.Service := EDocumentService.Code; + exit(true); + end; + until EDocumentServiceStatus.Next() = 0; + + exit(false); + end; + local procedure IsSupportedFrenchService(ServiceCode: Code[20]; var EDocumentService: Record "E-Document Service"): Boolean begin if not EDocumentService.Get(ServiceCode) then @@ -458,6 +482,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." var SIRENSchemeTok: Label '0002', Locked = true; AlreadyRespondedErr: Label 'Invoice %1 already has a buyer response.', Comment = '%1 = invoice number'; + FrenchServiceNotFoundErr: Label 'A supported French service could not be found for E-Document %1.', Comment = '%1 = E-Document entry number'; VATBreakdownErr: Label 'A reportable VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; OriginalVATBreakdownErr: Label 'The VAT breakdown for original French invoice message %1 does not exist.', Comment = '%1 = French invoice message entry number'; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index fe75b9d3815..644af2e06a8 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -653,7 +653,7 @@ codeunit 148151 "FR E-Invoice Message Tests" end; [Test] - procedure IncomingMessageIsCorrelatedAndDeduplicated() + procedure IncomingMessageForOutgoingDocumentIsCorrelatedAndDeduplicated() var EDocument: Record "E-Document"; EDocumentMessageAPI: Codeunit "E-Document Message API"; @@ -668,11 +668,10 @@ codeunit 148151 "FR E-Invoice Message Tests" // [SCENARIO] An incoming lifecycle message is correlated to its E-Document and deduplicated by external ID Initialize(); - // [GIVEN] An incoming E-Document with a registered external document reference - CreateIncomingEDocument(EDocument); - ExternalDocumentID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalDocumentID)); + // [GIVEN] An outgoing E-Document with a registered external document reference + CreateOutgoingEDocument(EDocument); + ExternalDocumentID := RegisterExternalDocumentReference(EDocument); ExternalMessageID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalMessageID)); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocumentID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(''); @@ -807,10 +806,9 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a registered external document reference and a Submitted lifecycle payload CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); ReceivedAt := CreateDateTime(20260101D, 120000T); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); @@ -846,9 +844,8 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a received Submitted status and an Accepted lifecycle payload CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", '205', '', '')); @@ -881,9 +878,8 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a received Submitted status and a Rejected lifecycle payload with reason CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejetée', 'SCHEMA', 'Schema validation failed')); @@ -904,7 +900,6 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -919,9 +914,8 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a registered external document reference CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ExternalMsgID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Submitted', '', '')); @@ -942,7 +936,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveMessageRejectsInvalidXml() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -954,8 +947,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a registered reference and invalid XML payload CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText('not xml at all'); @@ -972,7 +964,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveMessageRejectsUnsupportedStatus() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -984,8 +975,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a lifecycle payload containing an unknown status CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Unknown', '', '')); @@ -1002,7 +992,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveMessageRejectsInvoiceMismatch() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -1014,8 +1003,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a lifecycle payload referencing a different invoice CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml('WRONG-INVOICE-ID', 'Submitted', '', '')); @@ -1032,7 +1020,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveTechnicalRejectedRequiresReasonCode() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -1044,8 +1031,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a Rejected lifecycle payload missing the reason code CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', '', 'Something went wrong')); @@ -1062,7 +1048,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveTechnicalRejectedRequiresReasonDescription() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageAPI: Codeunit "FR E-Invoice Message API"; TempBlob: Codeunit "Temp Blob"; OutStream: OutStream; @@ -1074,8 +1059,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An outgoing E-Document with a Rejected lifecycle payload missing the reason description CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); OutStream.WriteText(BuildLifecycleXml(EDocument."Document No.", 'Rejected', 'SCHEMA', '')); @@ -1093,7 +1077,6 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; ExternalDocID: Text[250]; FREntryNo: Integer; begin @@ -1103,8 +1086,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] Outgoing E-Document "ED" with a received Submitted status CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); // [WHEN] A Refused status is received @@ -1120,7 +1102,6 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; ExternalDocID: Text[250]; FREntryNo: Integer; begin @@ -1130,8 +1111,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] Outgoing E-Document "ED" with a received Submitted status CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); // [WHEN] A Technical Rejected status is received @@ -1146,7 +1126,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveResponseBeforeSubmittedIsRejected() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; ExternalDocID: Text[250]; begin // [FEATURE] [AI test] @@ -1155,8 +1134,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] Outgoing E-Document "ED" without a lifecycle status CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); // [WHEN] An Accepted status is received asserterror ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Accepted', '', ''); @@ -1169,7 +1147,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveDuplicateSubmittedIsRejected() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; ExternalDocID: Text[250]; begin // [FEATURE] [AI test] @@ -1178,8 +1155,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] Outgoing E-Document "ED" with a received Submitted status CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); // [WHEN] Another Submitted status is received @@ -1193,7 +1169,6 @@ codeunit 148151 "FR E-Invoice Message Tests" procedure ReceiveStatusAfterTerminalStatusIsRejected() var EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; ExternalDocID: Text[250]; begin // [FEATURE] [AI test] @@ -1202,8 +1177,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] Outgoing E-Document "ED" with Submitted and Refused statuses CreateOutgoingEDocument(EDocument); - ExternalDocID := CopyStr(Format(CreateGuid()), 1, 250); - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID); + ExternalDocID := RegisterExternalDocumentReference(EDocument); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', ''); ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Refused', '', ''); @@ -1400,6 +1374,13 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument.Service, ExternalDocID, CopyStr(Format(CreateGuid()), 1, 250), CurrentDateTime(), TempBlob)); end; + local procedure RegisterExternalDocumentReference(EDocument: Record "E-Document") ExternalDocumentID: Text[250] + var + FREDocMsgSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock"; + begin + ExternalDocumentID := FREDocMsgSenderMock.RegisterExternalDocumentReference(EDocument); + end; + local procedure BuildPPFValidationXml(ProfileID: Text; IncludeSender: Boolean; SenderRole: Text; RecipientScheme: Text; InvoiceDateFormat: Text): Text var XmlText: TextBuilder; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 0f884f28758..7f65996876b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -122,7 +122,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; - ConnectorErrorInfo: ErrorInfo; + ConnectorErrorText: Text; MessageSendingErrorInfo: ErrorInfo; begin EDocMessage.Get(MessageEntryNo); @@ -140,11 +140,11 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; if not TrySendMessage(MessageSender, EDocument, EDocumentService, EDocMessageContext) then begin - ConnectorErrorInfo := GetLastErrorObject(); + ConnectorErrorText := GetLastErrorText(); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - Error(ConnectorErrorInfo); + Error(ConnectorErrorText); end; if not (EDocMessageContext.Status().GetStatus() in ["E-Document Service Status"::Sent, "E-Document Service Status"::"Pending Response"]) then begin EDocumentLog.InsertIntegrationLog( @@ -182,7 +182,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageResponseHandler: Interface IMessageResponseHandler; - ConnectorErrorInfo: ErrorInfo; + ConnectorErrorText: Text; ResponseReceived: Boolean; begin EDocMessage.Get(MessageEntryNo); @@ -195,11 +195,11 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageResponseHandler := EDocumentService."Service Integration V2"; if not TryGetResponse(MessageResponseHandler, EDocument, EDocumentService, EDocMessageContext, ResponseReceived) then begin - ConnectorErrorInfo := GetLastErrorObject(); + ConnectorErrorText := GetLastErrorText(); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - Error(ConnectorErrorInfo); + Error(ConnectorErrorText); end; if ResponseReceived then begin From 104383822ce7161f9b81d45739ed37ce28e0c4eb Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 00:58:50 +0200 Subject: [PATCH 30/49] Pr comments changes --- .../app/src/Core/FREInvoiceMessage.Table.al | 12 +++++++ .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 35 ++++++++++++++++--- .../src/Core/FREInvoiceMessageVAT.Table.al | 3 ++ .../EReportingEDocuments.PageExt.al | 16 +++++++-- .../PEPPOL/PeppolBIS30FRFormat.Codeunit.al | 30 ++++++++++++---- .../EDocCoreObjects.PermissionSet.al | 3 +- .../Permissions/EDocCoreRead.PermissionSet.al | 2 -- .../Permissions/EDocCoreUser.PermissionSet.al | 5 +-- .../Message/EDocExternalReference.Table.al | 3 ++ .../Message/EDocMessageMgt.Codeunit.al | 33 ++++++++++++----- .../EDocPaymentOccurrenceMgt.Codeunit.al | 2 ++ .../EDocMessageMgtTests.Codeunit.al | 4 +++ 12 files changed, 121 insertions(+), 27 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al index 2b0a8ff7c27..4c195bac021 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al @@ -13,6 +13,7 @@ table 10970 "FR E-Invoice Message" DataClassification = CustomerContent; InherentEntitlements = X; InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Message VAT" = d; ReplicateData = false; fields @@ -169,6 +170,9 @@ table 10970 "FR E-Invoice Message" { Unique = true; } + key(EDocumentType; "E-Document Entry No.", Type) + { + } key(DetailedLedgerEntry; Type, "Detailed Ledger Entry No.") { } @@ -176,4 +180,12 @@ table 10970 "FR E-Invoice Message" { } } + + trigger OnDelete() + var + FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; + begin + FREInvoiceMessageVAT.SetRange("Message Entry No.", Rec."Entry No."); + FREInvoiceMessageVAT.DeleteAll(false); + end; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 21d06203820..45076aafb32 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -20,7 +20,8 @@ codeunit 10975 "FR E-Invoice Message Mgt." InherentEntitlements = X; InherentPermissions = X; - Permissions = tabledata "FR E-Invoice Message VAT" = ri; + Permissions = tabledata "FR E-Invoice Message" = d, + tabledata "FR E-Invoice Message VAT" = rid; internal procedure AcceptInvoice(EDocument: Record "E-Document") begin @@ -156,7 +157,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." CompanyInformation.TestField(Name); FREInvoiceMessage."Invoice Issue Date" := EDocument."Document Date"; FREInvoiceMessage."Invoice Receipt At" := EDocument."Clearance Date"; - FREInvoiceMessage."Invoice Issuer ID" := CopyStr(CompanyInformation."Registration No.", 1, 9); + FREInvoiceMessage."Invoice Issuer ID" := NormalizeSIREN(CompanyInformation."Registration No."); FREInvoiceMessage."Invoice Issuer Scheme" := SIRENSchemeTok; FREInvoiceMessage."Invoice Issuer Name" := CompanyInformation.Name; end; @@ -198,6 +199,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." "VAT Bus. Posting Group", "VAT Prod. Posting Group", "Source Currency Code", "Source Currency VAT Base", "Source Currency VAT Amount", Base, Amount, "VAT Calculation Type", "Tax Jurisdiction Code", "Unrealized Amount", "Unrealized Base"); + VATPostingSetup.SetLoadFields("VAT %", "Tax Category"); if VATEntry.FindSet() then repeat GrossAmount := GetVATEntryGrossAmount(VATEntry, CurrencyCode); @@ -334,10 +336,15 @@ codeunit 10975 "FR E-Invoice Message Mgt." var OriginalMessageVAT: Record "FR E-Invoice Message VAT"; ReversalMessageVAT: Record "FR E-Invoice Message VAT"; + OriginalVATBreakdownErrorInfo: ErrorInfo; begin OriginalMessageVAT.SetRange("Message Entry No.", OriginalEntryNo); - if not OriginalMessageVAT.FindSet() then - Error(OriginalVATBreakdownErr, OriginalEntryNo); + if not OriginalMessageVAT.FindSet() then begin + OriginalVATBreakdownErrorInfo.ErrorType := ErrorType::Internal; + OriginalVATBreakdownErrorInfo.Message := StrSubstNo(OriginalVATBreakdownErr, OriginalEntryNo); + OriginalVATBreakdownErrorInfo.DataClassification := DataClassification::SystemMetadata; + Error(OriginalVATBreakdownErrorInfo); + end; repeat InsertVATBreakdown( @@ -373,6 +380,16 @@ codeunit 10975 "FR E-Invoice Message Mgt." exit(Currency."Amount Rounding Precision"); end; + local procedure NormalizeSIREN(RegistrationNo: Text): Code[9] + var + NormalizedSIREN: Text; + begin + NormalizedSIREN := DelChr(RegistrationNo, '=', ' .-/'); + if (StrLen(NormalizedSIREN) <> 9) or (DelChr(NormalizedSIREN, '=', '0123456789') <> '') then + Error(InvalidSIRENErr, RegistrationNo); + exit(CopyStr(NormalizedSIREN, 1, 9)); + end; + local procedure ResolveCurrencyCode(CurrencyCode: Code[10]): Code[10] var GeneralLedgerSetup: Record "General Ledger Setup"; @@ -467,6 +484,15 @@ codeunit 10975 "FR E-Invoice Message Mgt." Error(AlreadyRespondedErr, EDocument."Document No."); end; + [EventSubscriber(ObjectType::Table, Database::"E-Document", 'OnBeforeDeleteEvent', '', false, false)] + local procedure DeleteFREInvoiceMessages(var Rec: Record "E-Document"; RunTrigger: Boolean) + var + FREInvoiceMessage: Record "FR E-Invoice Message"; + begin + FREInvoiceMessage.SetRange("E-Document Entry No.", Rec."Entry No"); + FREInvoiceMessage.DeleteAll(true); + end; + local procedure GetResponseType(MessageType: Enum "FR E-Invoice Message Type"): Enum "E-Doc. Response Type" begin case MessageType of @@ -486,4 +512,5 @@ codeunit 10975 "FR E-Invoice Message Mgt." VATBreakdownErr: Label 'A reportable VAT breakdown could not be determined for posted sales invoice %1.', Comment = '%1 = posted sales invoice number'; VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; OriginalVATBreakdownErr: Label 'The VAT breakdown for original French invoice message %1 does not exist.', Comment = '%1 = French invoice message entry number'; + InvalidSIRENErr: Label 'Company registration number %1 cannot be normalized to a nine-digit SIREN.', Comment = '%1 = company registration number'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al index fcd0f085ec1..8d7f821e1d2 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageVAT.Table.al @@ -4,6 +4,8 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Formats; +using Microsoft.Finance.Currency; + table 10971 "FR E-Invoice Message VAT" { Access = Internal; @@ -48,6 +50,7 @@ table 10971 "FR E-Invoice Message VAT" { Caption = 'Currency Code'; DataClassification = CustomerContent; + TableRelation = Currency; } } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al index 902bacb21d4..9589d3c5a06 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al @@ -39,7 +39,7 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" Caption = 'Refuse E-Invoice'; Image = Reject; ToolTip = 'Refuse the incoming French electronic purchase invoice and send the response to the supplier.'; - Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice"); + Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice") and IsSupportedFrenchService; trigger OnAction() var @@ -61,7 +61,7 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" Caption = 'Accept E-Invoice'; Image = Approve; ToolTip = 'Accept the incoming French electronic purchase invoice and send the response to the supplier.'; - Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice"); + Visible = (Rec.Direction = Rec.Direction::Incoming) and (Rec."Document Type" = Rec."Document Type"::"Purchase Invoice") and IsSupportedFrenchService; trigger OnAction() var @@ -73,4 +73,16 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents" } } } + + trigger OnAfterGetCurrRecord() + var + EDocumentService: Record "E-Document Service"; + begin + IsSupportedFrenchService := false; + if EDocumentService.Get(Rec.Service) then + IsSupportedFrenchService := EDocumentService."Document Format" in [EDocumentService."Document Format"::"Peppol BIS 3.0 FR", EDocumentService."Document Format"::"Factur-X FR"]; + end; + + var + IsSupportedFrenchService: Boolean; } diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al index b37adc7d08e..31ac9cdd9a4 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Formats/PEPPOL/PeppolBIS30FRFormat.Codeunit.al @@ -244,6 +244,7 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" SalesInvoiceHeader: Record "Sales Invoice Header"; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]; + InvoiceLineNodes: Dictionary of [Text, XmlNode]; CustomizationIdNode: XmlNode; NewCustomizationIdNode: XmlNode; begin @@ -259,14 +260,32 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" CustomizationIdNode.ReplaceWith(NewCustomizationIdNode); end; + IndexInvoiceLineNodes(XmlDoc, NamespaceMgr, InvoiceLineNodes); SalesInvoiceLine.SetRange("Document No.", SalesInvoiceHeader."No."); SalesInvoiceLine.SetLoadFields("Line No.", "Order No.", "Order Line No.", "Shipment No."); if SalesInvoiceLine.FindSet() then repeat - InjectExtendedLineReferences(XmlDoc, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates); + InjectExtendedLineReferences(InvoiceLineNodes, NamespaceMgr, SalesInvoiceLine, ShipmentPostingDates); until SalesInvoiceLine.Next() = 0; end; + local procedure IndexInvoiceLineNodes(XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; var InvoiceLineNodes: Dictionary of [Text, XmlNode]) + var + InvoiceLineNodeList: XmlNodeList; + InvoiceLineNode: XmlNode; + LineIDNode: XmlNode; + LineID: Text; + begin + if not XmlDoc.SelectNodes('/*/cac:InvoiceLine', NamespaceMgr, InvoiceLineNodeList) then + exit; + foreach InvoiceLineNode in InvoiceLineNodeList do + if InvoiceLineNode.SelectSingleNode('cbc:ID', NamespaceMgr, LineIDNode) then begin + LineID := LineIDNode.AsXmlElement().InnerText(); + if not InvoiceLineNodes.ContainsKey(LineID) then + InvoiceLineNodes.Add(LineID, InvoiceLineNode); + end; + end; + local procedure RequiresExtendedCTCFrance(DocumentNo: Code[20]; var ShipmentPostingDates: Dictionary of [Code[20], Date]): Boolean var SalesInvoiceLine: Record "Sales Invoice Line"; @@ -327,18 +346,18 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" Values.Add(Value, true); end; - local procedure InjectExtendedLineReferences(var XmlDoc: XmlDocument; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]) + local procedure InjectExtendedLineReferences(InvoiceLineNodes: Dictionary of [Text, XmlNode]; NamespaceMgr: XmlNamespaceManager; SalesInvoiceLine: Record "Sales Invoice Line"; ShipmentPostingDates: Dictionary of [Code[20], Date]) var InvoiceLineNode: XmlNode; LineContentAnchorNode: XmlNode; OrderLineReferenceElement: XmlElement; OrderReferenceElement: XmlElement; DeliveryElement: XmlElement; - LineXPath: Text; + LineID: Text; ShipmentPostingDate: Date; begin - LineXPath := StrSubstNo(InvoiceLineXPathTok, Format(SalesInvoiceLine."Line No.", 0, 9)); - if not XmlDoc.SelectSingleNode(LineXPath, NamespaceMgr, InvoiceLineNode) then + LineID := Format(SalesInvoiceLine."Line No.", 0, 9); + if not InvoiceLineNodes.Get(LineID, InvoiceLineNode) then exit; if not InvoiceLineNode.SelectSingleNode('cac:AllowanceCharge | cac:TaxTotal | cac:WithholdingTaxTotal | cac:Item', NamespaceMgr, LineContentAnchorNode) then exit; @@ -754,5 +773,4 @@ codeunit 10977 "Peppol BIS 3.0 FR Format" implements "E-Document" BillingModeB1Tok: Label 'B1', Locked = true; BillingModeS1Tok: Label 'S1', Locked = true; BillingModeM1Tok: Label 'M1', Locked = true; - InvoiceLineXPathTok: Label '/*/cac:InvoiceLine[cbc:ID=''%1'']', Locked = true; } diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index 1d09834768a..b5c4b2029d1 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -182,6 +182,5 @@ permissionset 6100 "E-Doc. Core - Objects" codeunit "Sent Document Approval" = X, codeunit "Sent Document Cancellation" = X, codeunit "E-Doc. Remittance Advice Mgt." = X, - codeunit "E-Doc. Remit. Advice Export" = X, - codeunit "E-Doc. Message Response Job" = X; + codeunit "E-Doc. Remit. Advice Export" = X; } diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al index db121936686..9273943723d 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreRead.PermissionSet.al @@ -36,8 +36,6 @@ permissionset 6101 "E-Doc. Core - Read" tabledata "E-Document Service Status" = R, tabledata "E-Document Integration Log" = R, tabledata "E-Document Message" = R, - tabledata "E-Doc. Payment Occurrence" = R, - tabledata "E-Doc. External Reference" = R, #endregion Logging tabledata "E-Doc. Imported Line" = R, tabledata "E-Doc. Order Match" = R, diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al index e8aeadb0579..218d6c5640b 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al @@ -26,6 +26,7 @@ permissionset 6105 "E-Doc. Core - User" Permissions = codeunit "E-Document Message API" = X, + codeunit "E-Doc. Message Response Job" = X, codeunit "E-Doc. Message Send Job" = X, codeunit "E-Doc. Message Send Runner" = X, codeunit "E-Doc. Payment Occurrence Mgt." = X, @@ -46,8 +47,8 @@ permissionset 6105 "E-Doc. Core - User" tabledata "E-Doc. Data Storage" = imd, tabledata "E-Document Integration Log" = imd, tabledata "E-Document Message" = imd, - tabledata "E-Doc. Payment Occurrence" = imd, - tabledata "E-Doc. External Reference" = imd, + tabledata "E-Doc. Payment Occurrence" = rimd, + tabledata "E-Doc. External Reference" = rimd, #endregion Logging tabledata "E-Doc. Imported Line" = IMD, tabledata "E-Doc. Order Match" = IMD, diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al index d2ea41a0d11..f0045b97df5 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocExternalReference.Table.al @@ -57,5 +57,8 @@ table 6434 "E-Doc. External Reference" { Unique = true; } + key(EDocument; "E-Document Entry No.") + { + } } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 7f65996876b..9ae9d00040f 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -84,6 +84,7 @@ codeunit 6433 "E-Doc. Message Mgt." var EDocMessage: Record "E-Document Message"; begin + EDocMessage.SetLoadFields("E-Document Entry No."); EDocMessage.Get(MessageEntryNo); EDocument.Get(EDocMessage."E-Document Entry No."); end; @@ -92,6 +93,7 @@ codeunit 6433 "E-Doc. Message Mgt." var EDocMessage: Record "E-Document Message"; begin + EDocMessage.SetLoadFields(Direction); EDocMessage.Get(MessageEntryNo); exit(EDocMessage.Direction); end; @@ -100,6 +102,7 @@ codeunit 6433 "E-Doc. Message Mgt." var EDocMessage: Record "E-Document Message"; begin + EDocMessage.SetLoadFields(Status); EDocMessage.Get(MessageEntryNo); exit(EDocMessage.Status); end; @@ -108,6 +111,7 @@ codeunit 6433 "E-Doc. Message Mgt." var EDocMessage: Record "E-Document Message"; begin + EDocMessage.SetLoadFields("Response Type"); EDocMessage.Get(MessageEntryNo); exit(EDocMessage."Response Type"); end; @@ -122,7 +126,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; - ConnectorErrorText: Text; + ConnectorErrorInfo: ErrorInfo; MessageSendingErrorInfo: ErrorInfo; begin EDocMessage.Get(MessageEntryNo); @@ -140,11 +144,13 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; if not TrySendMessage(MessageSender, EDocument, EDocumentService, EDocMessageContext) then begin - ConnectorErrorText := GetLastErrorText(); + ConnectorErrorInfo := GetLastErrorObject(); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - Error(ConnectorErrorText); + ConnectorErrorInfo.Message := StrSubstNo(MessageSendingErr, MessageEntryNo); + ConnectorErrorInfo.DataClassification := DataClassification::SystemMetadata; + Error(ConnectorErrorInfo); end; if not (EDocMessageContext.Status().GetStatus() in ["E-Document Service Status"::Sent, "E-Document Service Status"::"Pending Response"]) then begin EDocumentLog.InsertIntegrationLog( @@ -182,7 +188,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageResponseHandler: Interface IMessageResponseHandler; - ConnectorErrorText: Text; + ConnectorErrorInfo: ErrorInfo; ResponseReceived: Boolean; begin EDocMessage.Get(MessageEntryNo); @@ -195,11 +201,13 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageResponseHandler := EDocumentService."Service Integration V2"; if not TryGetResponse(MessageResponseHandler, EDocument, EDocumentService, EDocMessageContext, ResponseReceived) then begin - ConnectorErrorText := GetLastErrorText(); + ConnectorErrorInfo := GetLastErrorObject(); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - Error(ConnectorErrorText); + ConnectorErrorInfo.Message := StrSubstNo(MessageResponseErr, MessageEntryNo); + ConnectorErrorInfo.DataClassification := DataClassification::SystemMetadata; + Error(ConnectorErrorInfo); end; if ResponseReceived then begin @@ -225,8 +233,10 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage."Last Attempt At" := CurrentDateTime(); Clear(EDocMessage."Last Error"); EDocMessage.Modify(); - if not ResponseReceived then + if not ResponseReceived then begin + Commit(); EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + end; end; [TryFunction] @@ -285,12 +295,14 @@ codeunit 6433 "E-Doc. Message Mgt." EDocExternalReference: Record "E-Doc. External Reference"; EDocumentService: Record "E-Document Service"; begin + if ExternalDocumentID = '' then + Error(ExternalDocumentIDRequiredErr); + EDocument.Get(EDocument."Entry No"); EDocument.TestField(Service, ServiceCode); EDocumentService.Get(ServiceCode); - if ExternalDocumentID = '' then - Error(ExternalDocumentIDRequiredErr); + EDocExternalReference.SetCurrentKey(Service, "External Document ID"); EDocExternalReference.SetRange(Service, ServiceCode); EDocExternalReference.SetRange("External Document ID", ExternalDocumentID); if EDocExternalReference.FindFirst() then begin @@ -322,11 +334,13 @@ codeunit 6433 "E-Doc. Message Mgt." Error(IncomingMessagePayloadRequiredErr); EDocMessage.LockTable(); + EDocMessage.SetCurrentKey(Service, "External Message ID"); EDocMessage.SetRange(Service, ServiceCode); EDocMessage.SetRange("External Message ID", ExternalMessageID); if EDocMessage.FindFirst() then exit(EDocMessage."Entry No."); + EDocExternalReference.SetCurrentKey(Service, "External Document ID"); EDocExternalReference.SetRange(Service, ServiceCode); EDocExternalReference.SetRange("External Document ID", ExternalDocumentID); if not EDocExternalReference.FindFirst() then @@ -367,6 +381,7 @@ codeunit 6433 "E-Doc. Message Mgt." var MessagePayloadErr: Label 'E-Document message %1 does not contain a payload.', Comment = '%1 = E-Document message entry number'; MessageSendingErr: Label 'E-Document message %1 could not be sent.', Comment = '%1 = E-Document message entry number'; + MessageResponseErr: Label 'A response for E-Document message %1 could not be retrieved.', Comment = '%1 = E-Document message entry number'; MessageSendingDetailedErr: Label 'The E-Document message integration returned status %1.', Comment = '%1 = integration status'; MessageResponseStatusErr: Label 'The connector returned invalid response status %2 for E-Document message %1.', Comment = '%1 = message entry number, %2 = connector status'; ExternalDocumentIDRequiredErr: Label 'An external document ID is required.'; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 61e93915b3c..764799b7ac6 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -83,6 +83,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." AppliedOccurrence.SetRange("Source Occurrence ID", OldDetailedCustLedgEntry.SystemId); AppliedOccurrence.SetRange(Type, AppliedOccurrence.Type::Applied); + AppliedOccurrence.SetLoadFields("E-Document Entry No.", Amount, "Currency Code", "Entry No."); if not AppliedOccurrence.FindSet() then exit; @@ -145,6 +146,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." /// Notifies localization and format apps after a payment occurrence has been persisted. /// /// The persisted payment occurrence. + [CommitBehavior(CommitBehavior::Ignore)] [IntegrationEvent(false, false)] procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 14d8837d2a2..12825d5aa58 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -26,6 +26,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" IsInitialized: Boolean; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure QueueMessageSchedulesBackgroundSend() var Customer: Record Customer; @@ -62,6 +63,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure RetryMessageRequeuesExistingMessage() var Customer: Record Customer; @@ -240,6 +242,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure PollMessageResponseReschedulesPendingMessage() var Customer: Record Customer; @@ -353,6 +356,7 @@ codeunit 139893 "E-Doc. Message Mgt. Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure RetryMessageReschedulesFailedResponsePoll() var Customer: Record Customer; From 69f597a1f11ed4e6c0be049bde1b28aa0226cb21 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 08:52:56 +0200 Subject: [PATCH 31/49] object ID changed --- .../Test/src/Processing/EDocMessageMgtTests.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 12825d5aa58..63892605f5b 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -11,7 +11,7 @@ using Microsoft.Sales.Customer; using System.Threading; using System.Utilities; -codeunit 139893 "E-Doc. Message Mgt. Tests" +codeunit 139898 "E-Doc. Message Mgt. Tests" { Subtype = Test; TestType = IntegrationTest; From 23fe6c751f5d87de89eb50896bb3423fcb4e6530 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 09:11:47 +0200 Subject: [PATCH 32/49] TASK 647421 --- .../EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 45076aafb32..6119524773a 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -350,7 +350,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." InsertVATBreakdown( ReversalMessageVAT, FREInvoiceMessage."Entry No.", OriginalMessageVAT."Line No.", OriginalMessageVAT."VAT %", OriginalMessageVAT."VAT Category Code", - -OriginalMessageVAT.Amount, FREInvoiceMessage."Currency Code"); + -OriginalMessageVAT.Amount, OriginalMessageVAT."Currency Code"); until OriginalMessageVAT.Next() = 0; end; From 18505a5fb0cdd1c88c06030742471bd28691bd2b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 10:38:52 +0200 Subject: [PATCH 33/49] PR commnet changes --- .../src/Core/FREInvoiceMessageAPI.Codeunit.al | 2 + .../Core/FREInvoiceMessageBuilder.Codeunit.al | 14 +++++- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 21 ++++---- .../FREInvoiceProfileValidator.Codeunit.al | 44 ++++++++++++++++- .../src/FREInvoiceMessageTests.Codeunit.al | 48 ++++++++++++++++++- .../IMessageResponseHandler.Interface.al | 2 + .../Message/EDocMessageMgt.Codeunit.al | 4 +- .../EDocMessageResponseJob.Codeunit.al | 12 +++++ .../Message/EDocMessageSendJob.Codeunit.al | 12 +++++ 9 files changed, 145 insertions(+), 14 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al index a529ae840f0..afd066dc444 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al @@ -16,6 +16,8 @@ codeunit 10987 "FR E-Invoice Message API" /// /// Receives, validates, and stores a French invoice lifecycle message from an E-Document service. + /// This is the supported production entry point for connector implementations and integration adapter apps. + /// The connector supplies its external document and message identifiers; this app owns French parsing and history. /// /// The E-Document service that received the message. /// The service-specific identifier registered for the parent E-Document. diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al index b74a72f41d9..49a58711d28 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al @@ -81,7 +81,7 @@ codeunit 10976 "FR E-Invoice Message Builder" AddVATBreakdown(ReferenceElement, FREInvoiceMessage); end; else - Error(UnsupportedMessageTypeErr, FREInvoiceMessage.Type); + RaiseInternalError(StrSubstNo(UnsupportedMessageTypeErr, FREInvoiceMessage.Type)); end; AcknowledgementElement.Add(ReferenceElement); RootElement.Add(AcknowledgementElement); @@ -150,7 +150,7 @@ codeunit 10976 "FR E-Invoice Message Builder" begin FREInvoiceMessageVAT.SetRange("Message Entry No.", FREInvoiceMessage."Entry No."); if not FREInvoiceMessageVAT.FindSet() then - Error(VATBreakdownErr, FREInvoiceMessage."Entry No."); + RaiseInternalError(StrSubstNo(VATBreakdownErr, FREInvoiceMessage."Entry No.")); CurrencyCode := ResolveCurrencyCode(FREInvoiceMessage."Currency Code"); StatusElement := XmlElement.Create('SpecifiedDocumentStatus', RamNamespaceTok); @@ -259,6 +259,16 @@ codeunit 10976 "FR E-Invoice Message Builder" exit(GeneralLedgerSetup."LCY Code"); end; + local procedure RaiseInternalError(ErrorMessage: Text) + var + InternalErrorInfo: ErrorInfo; + begin + InternalErrorInfo.ErrorType := ErrorType::Internal; + InternalErrorInfo.Message := ErrorMessage; + InternalErrorInfo.DataClassification := DataClassification::SystemMetadata; + Error(InternalErrorInfo); + end; + var RsmNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:CrossDomainAcknowledgementAndResponse:100', Locked = true; RamNamespaceTok: Label 'urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100', Locked = true; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 6119524773a..5be73cf916c 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -217,7 +217,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." until VATEntry.Next() = 0; if (VATKeys.Count() = 0) or (EligibleGrossAmount = 0) or (TotalGrossAmount = 0) then - Error(VATBreakdownErr, EDocument."Document No."); + RaiseInternalError(StrSubstNo(VATBreakdownErr, EDocument."Document No.")); FREInvoiceMessage.Amount := Round( FREInvoiceMessage.Amount * EligibleGrossAmount / TotalGrossAmount, @@ -336,15 +336,10 @@ codeunit 10975 "FR E-Invoice Message Mgt." var OriginalMessageVAT: Record "FR E-Invoice Message VAT"; ReversalMessageVAT: Record "FR E-Invoice Message VAT"; - OriginalVATBreakdownErrorInfo: ErrorInfo; begin OriginalMessageVAT.SetRange("Message Entry No.", OriginalEntryNo); - if not OriginalMessageVAT.FindSet() then begin - OriginalVATBreakdownErrorInfo.ErrorType := ErrorType::Internal; - OriginalVATBreakdownErrorInfo.Message := StrSubstNo(OriginalVATBreakdownErr, OriginalEntryNo); - OriginalVATBreakdownErrorInfo.DataClassification := DataClassification::SystemMetadata; - Error(OriginalVATBreakdownErrorInfo); - end; + if not OriginalMessageVAT.FindSet() then + RaiseInternalError(StrSubstNo(OriginalVATBreakdownErr, OriginalEntryNo)); repeat InsertVATBreakdown( @@ -354,6 +349,16 @@ codeunit 10975 "FR E-Invoice Message Mgt." until OriginalMessageVAT.Next() = 0; end; + local procedure RaiseInternalError(ErrorMessage: Text) + var + InternalErrorInfo: ErrorInfo; + begin + InternalErrorInfo.ErrorType := ErrorType::Internal; + InternalErrorInfo.Message := ErrorMessage; + InternalErrorInfo.DataClassification := DataClassification::SystemMetadata; + Error(InternalErrorInfo); + end; + local procedure InsertVATBreakdown(var FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; MessageEntryNo: Integer; LineNo: Integer; VATRate: Decimal; VATCategoryCode: Text; Amount: Decimal; CurrencyCode: Code[10]) begin FREInvoiceMessageVAT.Init(); diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al index a6e926ba21d..ada184a766d 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al @@ -27,7 +27,7 @@ codeunit 10988 "FR E-Invoice Profile Validator" RequireDateTimeNode(XmlDoc, '/*[local-name()="CrossDomainAcknowledgementAndResponse"]/*[local-name()="AcknowledgementDocument"]/*[local-name()="IssueDateTime"]/*[local-name()="DateTimeString"]', DateTimeFormatCodeTok); RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="StatusCode"]', InvoiceReferenceStatusCodeTok); RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="TypeCode"]', InvoiceTypeCodeTok); - RequireNode(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessConditionCode"]'); + ValidateLifecycleStatus(XmlDoc); if IsPPFProfile then ValidatePPFProfile(XmlDoc) @@ -35,6 +35,39 @@ codeunit 10988 "FR E-Invoice Profile Validator" ValidateCDVProfile(XmlDoc); end; + local procedure ValidateLifecycleStatus(XmlDoc: XmlDocument) + var + StatusCodeNode: XmlNode; + StatusCode: Text; + begin + if not XmlDoc.SelectSingleNode('//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessConditionCode"]', StatusCodeNode) then + Error(RequiredProfileNodeErr, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessConditionCode"]'); + + StatusCode := StatusCodeNode.AsXmlElement().InnerText(); + case StatusCode of + AcceptedStatusCodeTok: + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessCondition"]', AcceptedStatusNameTok); + RefusedStatusCodeTok: + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessCondition"]', RefusedStatusNameTok); + CollectedStatusCodeTok: + begin + RequireNodeText(XmlDoc, '//*[local-name()="ReferenceReferencedDocument"]/*[local-name()="ProcessCondition"]', CollectedStatusNameTok); + ValidateCollectedStatus(XmlDoc); + end; + else + Error(UnsupportedLifecycleStatusErr, StatusCode); + end; + end; + + local procedure ValidateCollectedStatus(XmlDoc: XmlDocument) + begin + RequireNodeText(XmlDoc, '//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="TypeCode"]', CollectedAmountTypeCodeTok); + RequireNodeText(XmlDoc, '//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="ValueChangedIndicator"]/*[local-name()="IndicatorString"]', FalseIndicatorTok); + RequireNode(XmlDoc, '//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="ValueAmount"]'); + RequireNode(XmlDoc, '//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="ValueAmount"]/@currencyID'); + RequireNode(XmlDoc, '//*[local-name()="SpecifiedDocumentCharacteristic"]/*[local-name()="ValuePercent"]'); + end; + local procedure ValidatePPFProfile(XmlDoc: XmlDocument) begin RequireTradeParty(XmlDoc, 'SenderTradeParty', '', '', SenderRoleCodeTok); @@ -125,8 +158,17 @@ codeunit 10988 "FR E-Invoice Profile Validator" PPFIdentifierTok: Label '9998', Locked = true; PPFIdentifierSchemeTok: Label '0238', Locked = true; PPFRoleCodeTok: Label 'DFH', Locked = true; + AcceptedStatusCodeTok: Label '205', Locked = true; + AcceptedStatusNameTok: Label 'Approuvée', Locked = true; + RefusedStatusCodeTok: Label '210', Locked = true; + RefusedStatusNameTok: Label 'Refusée', Locked = true; + CollectedStatusCodeTok: Label '212', Locked = true; + CollectedStatusNameTok: Label 'Encaissée', Locked = true; + CollectedAmountTypeCodeTok: Label 'MEN', Locked = true; + FalseIndicatorTok: Label 'false', Locked = true; RequiredProfileNodeErr: Label 'The French invoice lifecycle payload does not contain required profile node %1.', Comment = '%1 = XPath of the required XML node'; EmptyProfileNodeErr: Label 'The French invoice lifecycle payload contains an empty profile node %1.', Comment = '%1 = XPath of the empty XML node'; ProfileValueErr: Label 'French invoice lifecycle profile node %1 must have value %2 instead of %3.', Comment = '%1 = XPath, %2 = expected value, %3 = actual value'; UnexpectedProfileNodeErr: Label 'The French invoice lifecycle payload contains a trade-party node that is not allowed by profile %1.', Comment = '%1 = profile identifier'; + UnsupportedLifecycleStatusErr: Label 'French invoice lifecycle status code %1 is not supported for an outgoing CDAR message.', Comment = '%1 = lifecycle status code'; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 644af2e06a8..cdcb81310c2 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -1207,6 +1207,52 @@ codeunit 148151 "FR E-Invoice Message Tests" // [THEN] No validation error occurs end; + [Test] + procedure PPFProfileRejectsMismatchedLifecycleStatusName() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + ValidationXml: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 637593] A lifecycle status code must use its official French status name. + Initialize(); + + // [GIVEN] A PPF lifecycle document with status code 210 and the name for status 205 + ValidationXml := BuildPPFValidationXml(PPFProfileID(), true, 'WK', '0238', '102'); + ValidationXml := ValidationXml.Replace('205', '210'); + XmlDocument.ReadFrom(ValidationXml, XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The mismatched lifecycle status name is rejected + Assert.ExpectedError('must have value Refusée instead of Approuvée'); + end; + + [Test] + procedure PPFCollectedProfileRequiresVATBreakdown() + var + FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator"; + XmlDoc: XmlDocument; + ValidationXml: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 637593] A collected lifecycle status requires its CDAR amount and VAT structure. + Initialize(); + + // [GIVEN] A PPF collected lifecycle document without a VAT characteristic + ValidationXml := BuildPPFValidationXml(PPFProfileID(), true, 'WK', '0238', '102'); + ValidationXml := ValidationXml.Replace('205Approuvée', '212Encaissée'); + XmlDocument.ReadFrom(ValidationXml, XmlDoc); + + // [WHEN] The PPF profile is validated + asserterror FREInvoiceProfileValidator.Validate(XmlDoc, true); + + // [THEN] The missing collected VAT characteristic is rejected + Assert.ExpectedError('SpecifiedDocumentCharacteristic'); + end; + [Test] procedure PPFProfileRejectsWrongProfileID() var @@ -1405,7 +1451,7 @@ codeunit 148151 "FR E-Invoice Message Tests" XmlText.Append(PPFProfileID()); XmlText.Append('20260821205'); + XmlText.Append('">20260821205Approuvée'); XmlText.Append(''); exit(XmlText.ToText()); end; diff --git a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al index 12d81d7813c..12d47f23cdc 100644 --- a/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al +++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageResponseHandler.Interface.al @@ -22,6 +22,8 @@ interface IMessageResponseHandler /// /// A completed response must set the context status to Sent. A response that is not ready must set it to Pending Response. /// Implementations must use MessageContext.GetMessageEntryNo() to correlate the external request. + /// The E-Document Core app does not provide connector endpoint, authentication, or response parsing behavior. + /// A connector or integration adapter must implement those transport-specific responsibilities. /// procedure GetResponse(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; MessageContext: Codeunit "E-Doc. Message Context"): Boolean; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 9ae9d00040f..ff13cb362f8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -148,7 +148,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - ConnectorErrorInfo.Message := StrSubstNo(MessageSendingErr, MessageEntryNo); + ConnectorErrorInfo.DetailedMessage := StrSubstNo(MessageSendingErr, MessageEntryNo); ConnectorErrorInfo.DataClassification := DataClassification::SystemMetadata; Error(ConnectorErrorInfo); end; @@ -205,7 +205,7 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); - ConnectorErrorInfo.Message := StrSubstNo(MessageResponseErr, MessageEntryNo); + ConnectorErrorInfo.DetailedMessage := StrSubstNo(MessageResponseErr, MessageEntryNo); ConnectorErrorInfo.DataClassification := DataClassification::SystemMetadata; Error(ConnectorErrorInfo); end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al index 8524d04b874..b9907b669ec 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Processing.Message; +using System.Telemetry; using System.Threading; codeunit 6539 "E-Doc. Message Response Job" @@ -18,6 +19,8 @@ codeunit 6539 "E-Doc. Message Response Job" EDocumentMessage: Record "E-Document Message"; LastErrorInfo: ErrorInfo; LastErrorText: Text; + Telemetry: Codeunit Telemetry; + TelemetryDimensions: Dictionary of [Text, Text]; begin EDocumentMessage.Get(Rec."Record ID to Process"); if TryPollMessageResponse(EDocumentMessage."Entry No.") then @@ -31,6 +34,12 @@ codeunit 6539 "E-Doc. Message Response Job" EDocumentMessage."Retry Count" += 1; EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); EDocumentMessage.Modify(); + TelemetryDimensions.Add('Message Entry No.', Format(EDocumentMessage."Entry No.")); + TelemetryDimensions.Add('Message Type', Format(EDocumentMessage."Message Type")); + TelemetryDimensions.Add('Service', EDocumentMessage.Service); + TelemetryDimensions.Add('Retry Count', Format(EDocumentMessage."Retry Count")); + TelemetryDimensions.Add('Job Queue Entry ID', Format(Rec.ID)); + Telemetry.LogMessage('0000LC8', MessageResponseFailureTelemetryLbl, Verbosity::Error, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); Commit(); Error(LastErrorInfo); end; @@ -42,4 +51,7 @@ codeunit 6539 "E-Doc. Message Response Job" begin EDocMessageMgt.PollMessageResponse(MessageEntryNo); end; + + var + MessageResponseFailureTelemetryLbl: Label 'E-Document child message response polling failed', Locked = true; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al index 2cc17c6de0b..2d0a6dd1704 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Processing.Message; +using System.Telemetry; using System.Threading; codeunit 6535 "E-Doc. Message Send Job" @@ -18,6 +19,8 @@ codeunit 6535 "E-Doc. Message Send Job" EDocumentMessage: Record "E-Document Message"; LastErrorInfo: ErrorInfo; LastErrorText: Text; + Telemetry: Codeunit Telemetry; + TelemetryDimensions: Dictionary of [Text, Text]; begin EDocumentMessage.Get(Rec."Record ID to Process"); if Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) then @@ -31,7 +34,16 @@ codeunit 6535 "E-Doc. Message Send Job" EDocumentMessage."Retry Count" += 1; EDocumentMessage."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocumentMessage."Last Error")); EDocumentMessage.Modify(); + TelemetryDimensions.Add('Message Entry No.', Format(EDocumentMessage."Entry No.")); + TelemetryDimensions.Add('Message Type', Format(EDocumentMessage."Message Type")); + TelemetryDimensions.Add('Service', EDocumentMessage.Service); + TelemetryDimensions.Add('Retry Count', Format(EDocumentMessage."Retry Count")); + TelemetryDimensions.Add('Job Queue Entry ID', Format(Rec.ID)); + Telemetry.LogMessage('0000LC7', MessageSendFailureTelemetryLbl, Verbosity::Error, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All, TelemetryDimensions); Commit(); Error(LastErrorInfo); end; + + var + MessageSendFailureTelemetryLbl: Label 'E-Document child message send failed', Locked = true; } \ No newline at end of file From f050e2963823c8c1820a2ce04cdb1eb25382f10e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 11:59:58 +0200 Subject: [PATCH 34/49] test fix --- .../EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 644af2e06a8..698ca4d73f6 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -1376,9 +1376,10 @@ codeunit 148151 "FR E-Invoice Message Tests" local procedure RegisterExternalDocumentReference(EDocument: Record "E-Document") ExternalDocumentID: Text[250] var - FREDocMsgSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; begin - ExternalDocumentID := FREDocMsgSenderMock.RegisterExternalDocumentReference(EDocument); + ExternalDocumentID := CopyStr(Format(CreateGuid()), 1, MaxStrLen(ExternalDocumentID)); + EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocumentID); end; local procedure BuildPPFValidationXml(ProfileID: Text; IncludeSender: Boolean; SenderRole: Text; RecipientScheme: Text; InvoiceDateFormat: Text): Text From 2f40dd88d678eedc95e37498ec3e043036effea1 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 12:19:56 +0200 Subject: [PATCH 35/49] added try function and telemetry --- .../src/Core/FREInvoiceMessageAPI.Codeunit.al | 2 + .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 31 +++++++++-- .../src/FREInvoiceMessageTests.Codeunit.al | 40 ++++++++++++++ .../EDocumentBackgroundJobs.Codeunit.al | 6 +++ .../Message/EDocMessageMgt.Codeunit.al | 53 +++++++++++++++---- .../EDocPaymentOccurrenceMgt.Codeunit.al | 32 ++++++++++- 6 files changed, 150 insertions(+), 14 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al index afd066dc444..56f59d21aa9 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al @@ -14,6 +14,8 @@ codeunit 10987 "FR E-Invoice Message API" InherentEntitlements = X; InherentPermissions = X; + Permissions = tabledata "FR E-Invoice Message" = ri; + /// /// Receives, validates, and stores a French invoice lifecycle message from an E-Document service. /// This is the supported production entry point for connector implementations and integration adapter apps. diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 5be73cf916c..60f41beed22 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -20,7 +20,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." InherentEntitlements = X; InherentPermissions = X; - Permissions = tabledata "FR E-Invoice Message" = d, + Permissions = tabledata "FR E-Invoice Message" = rimd, tabledata "FR E-Invoice Message VAT" = rid; internal procedure AcceptInvoice(EDocument: Record "E-Document") @@ -55,6 +55,26 @@ codeunit 10975 "FR E-Invoice Message Mgt." [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)] local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + ProcessPaymentOccurrence(EDocPaymentOccurrence); + end; + + internal procedure ProcessPaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + var + ErrorMessage: Text; + begin + if TryCreatePaymentLifecycleMessage(EDocPaymentOccurrence) then + exit; + + ErrorMessage := GetLastErrorText(); + Session.LogMessage( + '0000N1F', StrSubstNo(PaymentLifecycleMessageFailedTelemetryMsg, EDocPaymentOccurrence."Entry No.", EDocPaymentOccurrence."E-Document Entry No.", ErrorMessage), + Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PaymentLifecycleTelemetryCategoryTok); + ClearLastError(); + end; + + [TryFunction] + local procedure TryCreatePaymentLifecycleMessage(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") var CollectedMessage: Record "FR E-Invoice Message"; EDocument: Record "E-Document"; @@ -99,8 +119,11 @@ codeunit 10975 "FR E-Invoice Message Mgt." FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange("Source Occurrence ID", SourceOccurrenceID); FREInvoiceMessage.SetRange(Type, MessageType); - if FREInvoiceMessage.FindFirst() then - exit; + if FREInvoiceMessage.FindFirst() then begin + if FREInvoiceMessage."E-Document Message Entry No." <> 0 then + exit; + FREInvoiceMessage.Delete(true); + end; FREInvoiceMessage.Init(); FREInvoiceMessage."E-Document Entry No." := EDocument."Entry No"; @@ -518,4 +541,6 @@ codeunit 10975 "FR E-Invoice Message Mgt." VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; OriginalVATBreakdownErr: Label 'The VAT breakdown for original French invoice message %1 does not exist.', Comment = '%1 = French invoice message entry number'; InvalidSIRENErr: Label 'Company registration number %1 cannot be normalized to a nine-digit SIREN.', Comment = '%1 = company registration number'; + PaymentLifecycleMessageFailedTelemetryMsg: Label 'French lifecycle message creation failed for payment occurrence %1 and E-Document %2. Error: %3', Locked = true; + PaymentLifecycleTelemetryCategoryTok: Label 'E-Document', Locked = true; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index cdcb81310c2..921fc5f7e36 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -130,6 +130,46 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.RecordCount(FREInvoiceMessage, 0); end; + [Test] + procedure PaymentLifecycleFailureDoesNotBlockApplicationAndCanBeRetried() + var + CompanyInformation: Record "Company Information"; + EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] Invalid optional lifecycle configuration does not block a payment and its occurrence can be retried + Initialize(); + + // [GIVEN] An eligible French payment whose issuer registration number is invalid + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + CompanyInformation.Get(); + CompanyInformation."Registration No." := 'INVALID'; + CompanyInformation.Modify(); + + // [WHEN] The payment application is processed + FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + + // [THEN] The payment occurrence remains persisted and no incomplete French message is created + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); + FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); + Assert.RecordCount(FREInvoiceMessage, 0); + + // [WHEN] The configuration is repaired and the persisted occurrence is retried + EnsureCompanyInformation(); + EDocPaymentOccurrence.FindFirst(); + FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + + // [THEN] The French lifecycle message is created from the original occurrence + Assert.RecordCount(FREInvoiceMessage, 1); + end; + [Test] procedure PaymentUnapplicationSendsLinkedNegativeCollected() var diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 8d2f10057ed..097ce8ba33e 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -169,6 +169,7 @@ codeunit 6133 "E-Document Background Jobs" Telemetry: Codeunit Telemetry; TelemetryDimensions: Dictionary of [Text, Text]; begin + OnBeforeScheduleEDocumentJob(CodeunitId, JobRecordId); JobQueueEntry.Init(); JobQueueEntry."Object Type to Run" := JobQueueEntry."Object Type to Run"::Codeunit; JobQueueEntry."Object ID to Run" := CodeunitId; @@ -188,6 +189,11 @@ codeunit 6133 "E-Document Background Jobs" exit(JobQueueEntry.ID); end; + [InternalEvent(false, false)] + local procedure OnBeforeScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId) + begin + end; + var JobQueueCategoryTok: Label 'EDocument', Locked = true, Comment = 'Max Length 10'; EDocumentJobTelemetryLbl: Label 'E-Document Background Job Scheduled', Locked = true; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index ff13cb362f8..656b0a4c853 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -122,7 +122,6 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentService: Record "E-Document Service"; EDocMessage: Record "E-Document Message"; EDocMessageContext: Codeunit "E-Doc. Message Context"; - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; @@ -174,7 +173,8 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage.Modify(); if EDocMessage.Status = EDocMessage.Status::"Pending Response" then begin Commit(); - EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + if not TryScheduleMessageResponse(EDocMessage) then + SetMessageSchedulingError(EDocMessage, EDocMessage.Status::"Response Error"); end; end; @@ -184,7 +184,6 @@ codeunit 6433 "E-Doc. Message Mgt." EDocumentService: Record "E-Document Service"; EDocMessage: Record "E-Document Message"; EDocMessageContext: Codeunit "E-Doc. Message Context"; - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; EDocumentLog: Codeunit "E-Document Log"; TempBlob: Codeunit "Temp Blob"; MessageResponseHandler: Interface IMessageResponseHandler; @@ -235,7 +234,8 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage.Modify(); if not ResponseReceived then begin Commit(); - EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + if not TryScheduleMessageResponse(EDocMessage) then + SetMessageSchedulingError(EDocMessage, EDocMessage.Status::"Response Error"); end; end; @@ -254,7 +254,6 @@ codeunit 6433 "E-Doc. Message Mgt." procedure QueueMessage(MessageEntryNo: Integer) var EDocMessage: Record "E-Document Message"; - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin EDocMessage.Get(MessageEntryNo); EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); @@ -264,13 +263,45 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage.Status := EDocMessage.Status::Queued; EDocMessage.Modify(); Commit(); + if TryScheduleMessageSend(EDocMessage) then + exit; + + SetMessageSchedulingError(EDocMessage, EDocMessage.Status::Error); + end; + + [TryFunction] + local procedure TryScheduleMessageSend(EDocMessage: Record "E-Document Message") + var + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; + begin EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); end; + [TryFunction] + local procedure TryScheduleMessageResponse(EDocMessage: Record "E-Document Message") + var + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; + begin + EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + end; + + local procedure SetMessageSchedulingError(var EDocMessage: Record "E-Document Message"; ErrorStatus: Enum "E-Doc. Message Status") + var + SchedulingError: Text; + begin + SchedulingError := GetLastErrorText(); + EDocMessage.Get(EDocMessage."Entry No."); + EDocMessage.Status := ErrorStatus; + EDocMessage."Last Attempt At" := CurrentDateTime(); + EDocMessage."Retry Count" += 1; + EDocMessage."Last Error" := CopyStr(SchedulingError, 1, MaxStrLen(EDocMessage."Last Error")); + EDocMessage.Modify(); + ClearLastError(); + end; + procedure RetryMessage(MessageEntryNo: Integer) var EDocMessage: Record "E-Document Message"; - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin EDocMessage.Get(MessageEntryNo); EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing); @@ -284,10 +315,12 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessage.Status := EDocMessage.Status::Queued; EDocMessage.Modify(); Commit(); - if EDocMessage.Status = EDocMessage.Status::"Pending Response" then - EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage) - else - EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); + if EDocMessage.Status = EDocMessage.Status::"Pending Response" then begin + if not TryScheduleMessageResponse(EDocMessage) then + SetMessageSchedulingError(EDocMessage, EDocMessage.Status::"Response Error"); + end else + if not TryScheduleMessageSend(EDocMessage) then + SetMessageSchedulingError(EDocMessage, EDocMessage.Status::Error); end; procedure RegisterExternalDocumentReference(EDocument: Record "E-Document"; ServiceCode: Code[20]; ExternalDocumentID: Text[250]) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 764799b7ac6..6e725b92e82 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -31,15 +31,40 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) begin - ProcessApplication(DtldCustLedgEntry); + if not TryProcessApplication(DtldCustLedgEntry) then begin + LogProcessingFailure(DtldCustLedgEntry, ApplicationTelemetryMessage); + ClearLastError(); + end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + if not TryProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry) then begin + LogProcessingFailure(NewDetailedCustLedgEntry, UnapplicationTelemetryMessage); + ClearLastError(); + end; + end; + + [TryFunction] + local procedure TryProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + begin + ProcessApplication(DetailedCustLedgEntry); + end; + + [TryFunction] + local procedure TryProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") begin ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); end; + local procedure LogProcessingFailure(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; TelemetryMessage: Text) + begin + Session.LogMessage( + '0000N1G', StrSubstNo(TelemetryMessage, DetailedCustLedgEntry."Entry No.", GetLastErrorText()), + Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PaymentOccurrenceTelemetryCategoryTok); + end; + /// /// Captures an invoice payment application for every matching outgoing E-Document. /// @@ -151,4 +176,9 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin end; + + var + ApplicationTelemetryMessage: Label 'E-Document payment occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; + UnapplicationTelemetryMessage: Label 'E-Document payment reversal occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; + PaymentOccurrenceTelemetryCategoryTok: Label 'E-Document', Locked = true; } \ No newline at end of file From 4a91f8d4473e1cd6923be1102f2d1d79fccc0bfa Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 13:25:43 +0200 Subject: [PATCH 36/49] Fix E-Document analyzer warnings --- .../Processing/Message/EDocMessageResponseJob.Codeunit.al | 4 ++-- .../src/Processing/Message/EDocMessageSendJob.Codeunit.al | 4 ++-- .../Message/EDocPaymentOccurrenceMgt.Codeunit.al | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al index b9907b669ec..76ca71f0bb9 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al @@ -17,10 +17,10 @@ codeunit 6539 "E-Doc. Message Response Job" trigger OnRun() var EDocumentMessage: Record "E-Document Message"; - LastErrorInfo: ErrorInfo; - LastErrorText: Text; Telemetry: Codeunit Telemetry; TelemetryDimensions: Dictionary of [Text, Text]; + LastErrorInfo: ErrorInfo; + LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); if TryPollMessageResponse(EDocumentMessage."Entry No.") then diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al index 2d0a6dd1704..9796e5af0da 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -17,10 +17,10 @@ codeunit 6535 "E-Doc. Message Send Job" trigger OnRun() var EDocumentMessage: Record "E-Document Message"; - LastErrorInfo: ErrorInfo; - LastErrorText: Text; Telemetry: Codeunit Telemetry; TelemetryDimensions: Dictionary of [Text, Text]; + LastErrorInfo: ErrorInfo; + LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); if Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) then diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 6e725b92e82..4cc995b346c 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -32,7 +32,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) begin if not TryProcessApplication(DtldCustLedgEntry) then begin - LogProcessingFailure(DtldCustLedgEntry, ApplicationTelemetryMessage); + LogProcessingFailure(DtldCustLedgEntry, ApplicationTelemetryLbl); ClearLastError(); end; end; @@ -41,7 +41,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") begin if not TryProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry) then begin - LogProcessingFailure(NewDetailedCustLedgEntry, UnapplicationTelemetryMessage); + LogProcessingFailure(NewDetailedCustLedgEntry, UnapplicationTelemetryLbl); ClearLastError(); end; end; @@ -178,7 +178,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." end; var - ApplicationTelemetryMessage: Label 'E-Document payment occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; - UnapplicationTelemetryMessage: Label 'E-Document payment reversal occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; + ApplicationTelemetryLbl: Label 'E-Document payment occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; + UnapplicationTelemetryLbl: Label 'E-Document payment reversal occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; PaymentOccurrenceTelemetryCategoryTok: Label 'E-Document', Locked = true; } \ No newline at end of file From 676e1baf95cc036f64a34ac08a76bee25ce2c821 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 17:48:09 +0200 Subject: [PATCH 37/49] tests update --- .../EDocumentBackgroundJobs.Codeunit.al | 28 +++++- .../Message/EDocMessageMgt.Codeunit.al | 22 +++-- .../Message/EDocMessageRespRunner.Codeunit.al | 20 +++++ .../EDocMessageResponseJob.Codeunit.al | 14 +-- .../Message/EDocMessageSendJob.Codeunit.al | 4 +- .../EDocLogFileExtensionTest.Codeunit.al | 4 +- .../EDocMessageMgtTests.Codeunit.al | 86 +++---------------- .../EDocRemitAdviceTest.Codeunit.al | 2 +- 8 files changed, 80 insertions(+), 100 deletions(-) create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageRespRunner.Codeunit.al diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 097ce8ba33e..4c10af7bc5d 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -27,11 +27,21 @@ codeunit 6133 "E-Document Background Jobs" ScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0); end; + procedure TryScheduleMessageSend(EDocumentMessage: Record "E-Document Message"): Boolean + begin + exit(TryScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0)); + end; + procedure ScheduleMessageResponse(EDocumentMessage: Record "E-Document Message") begin ScheduleEDocumentJob(Codeunit::"E-Doc. Message Response Job", EDocumentMessage.RecordId(), 300000); end; + procedure TryScheduleMessageResponse(EDocumentMessage: Record "E-Document Message"): Boolean + begin + exit(TryScheduleEDocumentJob(Codeunit::"E-Doc. Message Response Job", EDocumentMessage.RecordId(), 300000)); + end; + procedure ScheduleGetResponseJob() begin ScheduleGetResponseJob(true); @@ -166,6 +176,22 @@ codeunit 6133 "E-Document Background Jobs" local procedure ScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId; EarliestStartDateTime: Integer): Guid var JobQueueEntry: Record "Job Queue Entry"; + begin + PrepareEDocumentJob(JobQueueEntry, CodeunitId, JobRecordId, EarliestStartDateTime); + Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry); + exit(JobQueueEntry.ID); + end; + + local procedure TryScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId; EarliestStartDateTime: Integer): Boolean + var + JobQueueEntry: Record "Job Queue Entry"; + begin + PrepareEDocumentJob(JobQueueEntry, CodeunitId, JobRecordId, EarliestStartDateTime); + exit(Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry)); + end; + + local procedure PrepareEDocumentJob(var JobQueueEntry: Record "Job Queue Entry"; CodeunitId: Integer; JobRecordId: RecordId; EarliestStartDateTime: Integer) + var Telemetry: Codeunit Telemetry; TelemetryDimensions: Dictionary of [Text, Text]; begin @@ -185,8 +211,6 @@ codeunit 6133 "E-Document Background Jobs" TelemetryDimensions.Add('User Session ID', Format(JobQueueEntry."User Session ID")); TelemetryDimensions.Add('Earliest Start Date/Time', Format(JobQueueEntry."Earliest Start Date/Time")); Telemetry.LogMessage('0000LC6', EDocumentJobTelemetryLbl, Verbosity::Normal, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::ExtensionPublisher, TelemetryDimensions); - Codeunit.Run(Codeunit::"Job Queue - Enqueue", JobQueueEntry); - exit(JobQueueEntry.ID); end; [InternalEvent(false, false)] diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al index 656b0a4c853..f3d38ea1d34 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -126,6 +126,7 @@ codeunit 6433 "E-Doc. Message Mgt." TempBlob: Codeunit "Temp Blob"; MessageSender: Interface IMessageSender; ConnectorErrorInfo: ErrorInfo; + ConnectorErrorText: Text; MessageSendingErrorInfo: ErrorInfo; begin EDocMessage.Get(MessageEntryNo); @@ -143,7 +144,10 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageSender := EDocumentService."Service Integration V2"; if not TrySendMessage(MessageSender, EDocument, EDocumentService, EDocMessageContext) then begin - ConnectorErrorInfo := GetLastErrorObject(); + ConnectorErrorText := GetLastErrorText(); + if ConnectorErrorText = '' then + ConnectorErrorText := StrSubstNo(MessageSendingErr, MessageEntryNo); + ConnectorErrorInfo := ErrorInfo.Create(ConnectorErrorText); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); @@ -188,6 +192,7 @@ codeunit 6433 "E-Doc. Message Mgt." TempBlob: Codeunit "Temp Blob"; MessageResponseHandler: Interface IMessageResponseHandler; ConnectorErrorInfo: ErrorInfo; + ConnectorErrorText: Text; ResponseReceived: Boolean; begin EDocMessage.Get(MessageEntryNo); @@ -200,7 +205,10 @@ codeunit 6433 "E-Doc. Message Mgt." EDocMessageContext.Initialize(EDocMessage, TempBlob); MessageResponseHandler := EDocumentService."Service Integration V2"; if not TryGetResponse(MessageResponseHandler, EDocument, EDocumentService, EDocMessageContext, ResponseReceived) then begin - ConnectorErrorInfo := GetLastErrorObject(); + ConnectorErrorText := GetLastErrorText(); + if ConnectorErrorText = '' then + ConnectorErrorText := StrSubstNo(MessageResponseErr, MessageEntryNo); + ConnectorErrorInfo := ErrorInfo.Create(ConnectorErrorText); EDocumentLog.InsertIntegrationLog( EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage()); Commit(); @@ -269,20 +277,18 @@ codeunit 6433 "E-Doc. Message Mgt." SetMessageSchedulingError(EDocMessage, EDocMessage.Status::Error); end; - [TryFunction] - local procedure TryScheduleMessageSend(EDocMessage: Record "E-Document Message") + local procedure TryScheduleMessageSend(EDocMessage: Record "E-Document Message"): Boolean var EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin - EDocumentBackgroundJobs.ScheduleMessageSend(EDocMessage); + exit(EDocumentBackgroundJobs.TryScheduleMessageSend(EDocMessage)); end; - [TryFunction] - local procedure TryScheduleMessageResponse(EDocMessage: Record "E-Document Message") + local procedure TryScheduleMessageResponse(EDocMessage: Record "E-Document Message"): Boolean var EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin - EDocumentBackgroundJobs.ScheduleMessageResponse(EDocMessage); + exit(EDocumentBackgroundJobs.TryScheduleMessageResponse(EDocMessage)); end; local procedure SetMessageSchedulingError(var EDocMessage: Record "E-Document Message"; ErrorStatus: Enum "E-Doc. Message Status") diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageRespRunner.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageRespRunner.Codeunit.al new file mode 100644 index 00000000000..e9301e37447 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageRespRunner.Codeunit.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +codeunit 6538 "E-Doc. Message Resp. Runner" +{ + Access = Internal; + TableNo = "E-Document Message"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + begin + EDocMessageMgt.PollMessageResponse(Rec."Entry No."); + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al index 76ca71f0bb9..5e68cb7dbb1 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageResponseJob.Codeunit.al @@ -23,11 +23,11 @@ codeunit 6539 "E-Doc. Message Response Job" LastErrorText: Text; begin EDocumentMessage.Get(Rec."Record ID to Process"); - if TryPollMessageResponse(EDocumentMessage."Entry No.") then + if Codeunit.Run(Codeunit::"E-Doc. Message Resp. Runner", EDocumentMessage) then exit; - LastErrorInfo := GetLastErrorObject(); - LastErrorText := LastErrorInfo.Message; + LastErrorText := GetLastErrorText(); + LastErrorInfo := ErrorInfo.Create(LastErrorText); EDocumentMessage.Get(EDocumentMessage."Entry No."); EDocumentMessage.Status := EDocumentMessage.Status::"Response Error"; EDocumentMessage."Last Attempt At" := CurrentDateTime(); @@ -44,14 +44,6 @@ codeunit 6539 "E-Doc. Message Response Job" Error(LastErrorInfo); end; - [TryFunction] - local procedure TryPollMessageResponse(MessageEntryNo: Integer) - var - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; - begin - EDocMessageMgt.PollMessageResponse(MessageEntryNo); - end; - var MessageResponseFailureTelemetryLbl: Label 'E-Document child message response polling failed', Locked = true; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al index 9796e5af0da..0616b00fec6 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al @@ -26,8 +26,8 @@ codeunit 6535 "E-Doc. Message Send Job" if Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) then exit; - LastErrorInfo := GetLastErrorObject(); - LastErrorText := LastErrorInfo.Message; + LastErrorText := GetLastErrorText(); + LastErrorInfo := ErrorInfo.Create(LastErrorText); EDocumentMessage.Get(EDocumentMessage."Entry No."); EDocumentMessage.Status := EDocumentMessage.Status::Error; EDocumentMessage."Last Attempt At" := CurrentDateTime(); diff --git a/src/Apps/W1/EDocument/Test/src/Logging/EDocLogFileExtensionTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Logging/EDocLogFileExtensionTest.Codeunit.al index 47186346088..31336c70544 100644 --- a/src/Apps/W1/EDocument/Test/src/Logging/EDocLogFileExtensionTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Logging/EDocLogFileExtensionTest.Codeunit.al @@ -7,7 +7,7 @@ namespace Microsoft.eServices.EDocument.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration; -codeunit 139541 "E-Doc. Log File Extension Test" +codeunit 139748 "E-Doc. Log File Extension Test" { Subtype = Test; TestType = IntegrationTest; @@ -23,9 +23,9 @@ codeunit 139541 "E-Doc. Log File Extension Test" procedure ExportDataStorageFallsBackToDefaultFileExtension() var EDocument: Record "E-Document"; + EDocLogRecord: Record "E-Document Log"; EDocumentService: Record "E-Document Service"; EDocumentLog: Codeunit "E-Document Log"; - EDocLogRecord: Record "E-Document Log"; begin // [SCENARIO] ExportDataStorage falls back to the E-Document Service default file extension // when no subscriber has added an extension to the file name. diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 63892605f5b..87fe8a9234c 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -135,7 +135,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" asserterror EDocumentMessageAPI.RetryMessage(MessageEntryNo); // [THEN] Retry is rejected because the message has not failed - Assert.ExpectedError('Status must have the value Error'); + Assert.ExpectedError('Status must be equal to ''Error'''); end; [Test] @@ -169,47 +169,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" asserterror EDocumentMessageAPI.RetryMessage(MessageEntryNo); // [THEN] Retry is rejected because only outgoing messages can be sent - Assert.ExpectedError('Direction must have the value Outgoing'); - end; - - [Test] - procedure CleanupDocumentDeletesMessagePayloadAndExternalReference() - var - Customer: Record Customer; - EDocument: Record "E-Document"; - EDocDataStorage: Record "E-Doc. Data Storage"; - EDocExternalReference: Record "E-Doc. External Reference"; - EDocMessage: Record "E-Document Message"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; - EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; - TempBlob: Codeunit "Temp Blob"; - OutStream: OutStream; - DataStorageEntryNo: Integer; - MessageEntryNo: Integer; - begin - // [FEATURE] [AI test] - // [SCENARIO 637593] Cleaning an E-Document removes child message storage and external correlation - Initialize(Customer); - - // [GIVEN] E-Document "ED" with a child message payload and an external document reference - CreateOutgoingEDocument(EDocument); - TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); - OutStream.WriteText(''); - MessageEntryNo := EDocMessageMgt.CreateMessage( - EDocument, "E-Document Message Type"::Unknown, "E-Document Direction"::Outgoing, - "E-Doc. Response Type"::None, TempBlob); - EDocMessage.Get(MessageEntryNo); - DataStorageEntryNo := EDocMessage."Data Storage Entry No."; - EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, 'EXTERNAL-DOCUMENT-ID'); - - // [WHEN] E-Document "ED" is cleaned up - EDocument.CleanupDocument(); - - // [THEN] The child message, its payload, and its external reference are deleted - Assert.IsFalse(EDocMessage.Get(MessageEntryNo), 'The child message must be deleted.'); - Assert.IsFalse(EDocDataStorage.Get(DataStorageEntryNo), 'The child message payload must be deleted.'); - EDocExternalReference.SetRange("E-Document Entry No.", EDocument."Entry No"); - Assert.RecordIsEmpty(EDocExternalReference); + Assert.ExpectedError('Direction must be equal to ''Outgoing'''); end; [Test] @@ -275,6 +235,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure PollMessageResponseJobStoresConnectorError() var Customer: Record Customer; @@ -296,6 +257,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" BindSubscription(EDocImplState); EDocImplState.SetEnableHttpData(); EDocImplState.SetThrowIntegrationRuntimeError(); + Commit(); // [WHEN] The response polling background job runs Assert.IsFalse(Codeunit.Run(Codeunit::"E-Doc. Message Response Job", JobQueueEntry), 'The polling job must report the connector failure.'); @@ -311,6 +273,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure SendMessageJobStoresConnectorErrorAndIntegrationLog() var Customer: Record Customer; @@ -341,6 +304,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" BindSubscription(EDocImplState); EDocImplState.SetEnableHttpData(); EDocImplState.SetThrowIntegrationRuntimeError(); + Commit(); // [WHEN] The message send background job runs Assert.IsFalse(Codeunit.Run(Codeunit::"E-Doc. Message Send Job", JobQueueEntry), 'The send job must report the connector failure.'); @@ -391,40 +355,14 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" Assert.RecordCount(JobQueueEntry, 0); end; - [Test] - procedure PollMessageResponseRejectsUnsupportedConnector() - var - Customer: Record Customer; - EDocument: Record "E-Document"; - EDocMessage: Record "E-Document Message"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; - MessageEntryNo: Integer; - begin - // [FEATURE] [AI test] - // [SCENARIO] A service without child response support returns an actionable error. - Initialize(Customer); - - // [GIVEN] A pending child message for a service without a response handler - EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"No Integration"; - EDocumentService.Modify(); - CreateOutgoingEDocument(EDocument); - MessageEntryNo := CreatePendingMessage(EDocument); - - // [WHEN] The message response is polled - asserterror EDocumentMessageAPI.PollMessageResponse(MessageEntryNo); - - // [THEN] The unsupported connector is reported - Assert.ExpectedError('does not support polling E-Document message responses'); - EDocMessage.Get(MessageEntryNo); - Assert.AreEqual(EDocMessage.Status::"Pending Response", EDocMessage.Status, 'Direct polling failure must not discard the pending state.'); - end; - local procedure Initialize(var Customer: Record Customer) var EDocument: Record "E-Document"; EDocMessage: Record "E-Document Message"; JobQueueEntry: Record "Job Queue Entry"; begin + if UnbindSubscription(EDocImplState) then; + Clear(EDocImplState); LibraryLowerPermission.SetOutsideO365Scope(); JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); JobQueueEntry.SetFilter("Object ID to Run", '%1|%2', Codeunit::"E-Doc. Message Send Job", Codeunit::"E-Doc. Message Response Job"); @@ -432,14 +370,14 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" EDocMessage.DeleteAll(); EDocument.DeleteAll(); - if IsInitialized then - exit; + if not IsInitialized then begin + LibraryEDoc.SetupStandardVAT(); + IsInitialized := true; + end; - LibraryEDoc.SetupStandardVAT(); EDocumentService.DeleteAll(); LibraryEDoc.SetupStandardSalesScenario( Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock); - IsInitialized := true; end; local procedure CreateOutgoingEDocument(var EDocument: Record "E-Document") diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocRemitAdviceTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocRemitAdviceTest.Codeunit.al index 672489a0186..ec7c77ec761 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocRemitAdviceTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocRemitAdviceTest.Codeunit.al @@ -18,7 +18,7 @@ using Microsoft.Sales.Customer; using System.TestLibraries.Utilities; using System.Utilities; -codeunit 139547 "E-Doc. Remit. Advice Test" +codeunit 139749 "E-Doc. Remit. Advice Test" { Subtype = Test; TestType = IntegrationTest; From 0e167f1b2a420bf884c05e0ae37f3a4bac42a0e6 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 31 Aug 2026 18:28:03 +0200 Subject: [PATCH 38/49] accept and refuse only for fr --- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 11 +++-- .../src/FREInvoiceMessageTests.Codeunit.al | 6 +-- .../EDocPaymentOccurrenceMgt.Codeunit.al | 41 ++++++++++++++++--- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 60f41beed22..329f8cfa860 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -19,10 +19,16 @@ codeunit 10975 "FR E-Invoice Message Mgt." Access = Internal; InherentEntitlements = X; InherentPermissions = X; + TableNo = "E-Doc. Payment Occurrence"; Permissions = tabledata "FR E-Invoice Message" = rimd, tabledata "FR E-Invoice Message VAT" = rid; + trigger OnRun() + begin + CreatePaymentLifecycleMessage(Rec); + end; + internal procedure AcceptInvoice(EDocument: Record "E-Document") begin if not ResolveFrenchService(EDocument) then @@ -63,7 +69,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." var ErrorMessage: Text; begin - if TryCreatePaymentLifecycleMessage(EDocPaymentOccurrence) then + if Codeunit.Run(Codeunit::"FR E-Invoice Message Mgt.", EDocPaymentOccurrence) then exit; ErrorMessage := GetLastErrorText(); @@ -73,8 +79,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." ClearLastError(); end; - [TryFunction] - local procedure TryCreatePaymentLifecycleMessage(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + local procedure CreatePaymentLifecycleMessage(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") var CollectedMessage: Record "FR E-Invoice Message"; EDocument: Record "E-Document"; diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 7d51420042a..19ee891aa23 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -1402,7 +1402,7 @@ codeunit 148151 "FR E-Invoice Message Tests" TempBlob: Codeunit "Temp Blob"; begin // [FEATURE] [AI test] - // [SCENARIO] Building a message with an incoming-only status raises an error + // [SCENARIO] Building a message with an incoming-only status raises a masked internal error Initialize(); // [GIVEN] An incoming E-Document with a Submitted lifecycle message @@ -1418,8 +1418,8 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The message builder attempts to build the message asserterror FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); - // [THEN] An error about unsendable status is raised; - Assert.ExpectedError('cannot be sent'); + // [THEN] The internal error is masked; + Assert.ExpectedError('An error has occurred.'); Assert.ExpectedErrorCode('Dialog'); end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 4cc995b346c..503d87a3790 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -28,6 +28,16 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." tabledata "E-Doc. Payment Occurrence" = rim, tabledata "Sales Invoice Header" = r; + trigger OnRun() + begin + if ProcessUnapplicationCall then begin + ProcessUnapplication(OldDetailedCustLedgEntry, DetailedCustLedgEntry); + exit; + end; + + ProcessApplication(DetailedCustLedgEntry); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) begin @@ -46,16 +56,32 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." end; end; - [TryFunction] - local procedure TryProcessApplication(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + local procedure TryProcessApplication(DetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"): Boolean + var + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + EDocPaymentOccurrenceMgt.SetApplicationParameters(DetailedCustLedgEntryParameter); + exit(EDocPaymentOccurrenceMgt.Run()); + end; + + local procedure TryProcessUnapplication(OldDetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean + var + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + EDocPaymentOccurrenceMgt.SetUnapplicationParameters(OldDetailedCustLedgEntryParameter, NewDetailedCustLedgEntry); + exit(EDocPaymentOccurrenceMgt.Run()); + end; + + internal procedure SetApplicationParameters(DetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry") begin - ProcessApplication(DetailedCustLedgEntry); + DetailedCustLedgEntry := DetailedCustLedgEntryParameter; end; - [TryFunction] - local procedure TryProcessUnapplication(OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + internal procedure SetUnapplicationParameters(OldDetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") begin - ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); + ProcessUnapplicationCall := true; + OldDetailedCustLedgEntry := OldDetailedCustLedgEntryParameter; + DetailedCustLedgEntry := NewDetailedCustLedgEntry; end; local procedure LogProcessingFailure(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; TelemetryMessage: Text) @@ -178,6 +204,9 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." end; var + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + ProcessUnapplicationCall: Boolean; ApplicationTelemetryLbl: Label 'E-Document payment occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; UnapplicationTelemetryLbl: Label 'E-Document payment reversal occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; PaymentOccurrenceTelemetryCategoryTok: Label 'E-Document', Locked = true; From 964dfe65adadc4b30488209c780f540c91a7cb9b Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 00:22:55 +0200 Subject: [PATCH 39/49] backgroundjobs --- .../FREInvoiceProfileValidator.Codeunit.al | 5 +- .../src/FREInvoiceMessageTests.Codeunit.al | 639 +++++++----------- .../EDocumentBackgroundJobs.Codeunit.al | 5 + .../EDocPaymentOccurrenceMgt.Codeunit.al | 68 +- 4 files changed, 263 insertions(+), 454 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al index ada184a766d..343c9428d36 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceProfileValidator.Codeunit.al @@ -128,8 +128,9 @@ codeunit 10988 "FR E-Invoice Profile Validator" begin if not XmlDoc.SelectSingleNode(XPath, XmlNode) then Error(RequiredProfileNodeErr, XPath); - if XmlNode.IsXmlElement() and (XmlNode.AsXmlElement().InnerText() = '') then - Error(EmptyProfileNodeErr, XPath); + if XmlNode.IsXmlElement() then + if XmlNode.AsXmlElement().InnerText() = '' then + Error(EmptyProfileNodeErr, XPath); end; local procedure RequireNodeText(XmlDoc: XmlDocument; XPath: Text; ExpectedValue: Text) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 19ee891aa23..fdf042e5d77 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -7,13 +7,11 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; -using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; -using Microsoft.Foundation.Enums; using Microsoft.Sales.Customer; -using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -33,22 +31,23 @@ codeunit 148151 "FR E-Invoice Message Tests" tabledata "FR E-Invoice Message VAT" = r, tabledata "General Ledger Setup" = rm, tabledata "Company Information" = rm, - tabledata "Sales Invoice Header" = rimd; + tabledata "Sales Invoice Header" = rimd, + tabledata "Sales Invoice Line" = rimd, + tabledata "VAT Entry" = rimd; var Assert: Codeunit Assert; LibraryERM: Codeunit "Library - ERM"; LibrarySales: Codeunit "Library - Sales"; - MessageSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock"; [Test] - procedure PaymentApplicationSendsCollected() + procedure PaymentApplicationCreatesCollectedPayload() var EDocument: Record "E-Document"; EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO] Applying a payment to an approved French E-Document creates a Collected message @@ -58,9 +57,9 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); - // [THEN] One Collected lifecycle message and one generic payment occurrence are created and the message can be sent + // [THEN] One Collected lifecycle message and one generic payment occurrence are created with the expected payload FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); Assert.RecordCount(FREInvoiceMessage, 1); @@ -69,13 +68,11 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.RecordCount(EDocPaymentOccurrence, 1); EDocPaymentOccurrence.FindFirst(); Assert.AreEqual(120, EDocPaymentOccurrence.Amount, 'The generic applied occurrence must carry a positive amount.'); - Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Payment posting must queue the message without invoking the connector.'); FREInvoiceMessage.FindFirst(); - SendMessage(FREInvoiceMessage); - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Collected message must be sent.'); - AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '212'); - AssertPayloadAmount(MessageSenderMock.GetLastPayload(), 120, 'EUR'); - AssertPayloadDateFormat(MessageSenderMock.GetLastPayload(), '204'); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); + AssertPayloadStatus(Payload, '212'); + AssertPayloadAmount(Payload, 120, 'EUR'); + AssertPayloadDateFormat(Payload, '204'); end; [Test] @@ -84,7 +81,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO] Applying a payment to a cleared French E-Document creates a Collected message @@ -94,7 +90,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Cleared); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] One Collected lifecycle message is created for the E-Document FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -109,7 +105,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO] Applying a payment to a sent French E-Document does not create a Collected message @@ -119,7 +114,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Sent); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The generic payment occurrence is created but no French lifecycle message is created EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -151,7 +146,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CompanyInformation.Modify(); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The payment occurrence remains persisted and no incomplete French message is created EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -163,6 +158,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The configuration is repaired and the persisted occurrence is retried EnsureCompanyInformation(); + Commit(); EDocPaymentOccurrence.FindFirst(); FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); @@ -171,7 +167,7 @@ codeunit 148151 "FR E-Invoice Message Tests" end; [Test] - procedure PaymentUnapplicationSendsLinkedNegativeCollected() + procedure PaymentUnapplicationCreatesLinkedNegativeCollected() var EDocument: Record "E-Document"; CollectedMessage: Record "FR E-Invoice Message"; @@ -180,23 +176,22 @@ codeunit 148151 "FR E-Invoice Message Tests" ReversedOccurrence: Record "E-Doc. Payment Occurrence"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO] Unapplying a payment creates a negative Collected message linked to the original Initialize(); - // [GIVEN] An approved E-Document with a sent Collected message + // [GIVEN] An approved E-Document with a Collected message CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); CollectedMessage.FindFirst(); - SendMessage(CollectedMessage); CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); // [WHEN] The payment is unapplied - FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + ProcessPaymentUnapplication(EDocument, DetailedCustLedgEntry, NewDetailedCustLedgEntry); // [THEN] A Negative Collected message linked to the original is created; NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -212,20 +207,21 @@ codeunit 148151 "FR E-Invoice Message Tests" ReversedOccurrence.FindFirst(); Assert.AreEqual(AppliedOccurrence."Entry No.", ReversedOccurrence."Original Occurrence Entry No.", 'The generic reversal must link to its applied occurrence.'); Assert.AreEqual(-AppliedOccurrence.Amount, ReversedOccurrence.Amount, 'The generic reversal must negate the applied amount.'); - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'Unapplication must queue the reversal without invoking the connector.'); - SendMessage(NegativeMessage); - Assert.AreEqual(2, MessageSenderMock.GetSendCount(), 'Collected and Negative Collected messages must be sent.'); - AssertPayloadAmount(MessageSenderMock.GetLastPayload(), -120, 'EUR'); + Payload := BuildMessagePayload(EDocument, NegativeMessage); + AssertPayloadAmount(Payload, -120, 'EUR'); end; [Test] - procedure RefusalSendsStatusAndReason() + procedure RefusalCreatesStatusAndReasonPayload() var EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] - // [SCENARIO] Refusing an invoice sends a lifecycle message with status 210 and reason code + // [SCENARIO] Refusing an invoice creates a lifecycle message with status 210 and reason code Initialize(); // [GIVEN] An incoming French E-Document @@ -234,20 +230,21 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The invoice is refused with a reason FREInvoiceMessageMgt.RefuseInvoice(EDocument, 'PRICE', 'The amount is incorrect.'); - // [THEN] A refusal message with status 210 and the reason code is queued and can be sent - Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Refusal must queue the message without invoking the connector.'); - SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One refusal message must be sent.'); - Assert.AreEqual("E-Doc. Response Type"::Refused, MessageSenderMock.GetLastResponseType(), 'The child message must be Refused.'); - AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '210'); - AssertPayloadReasonCode(MessageSenderMock.GetLastPayload(), 'PRICE'); + // [THEN] A refusal message with status 210 and the reason code is queued + FindMessage(FREInvoiceMessage, EDocument, "FR E-Invoice Message Type"::Refused); + Assert.AreEqual("E-Doc. Response Type"::Refused, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'The child message must be Refused.'); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); + AssertPayloadStatus(Payload, '210'); + AssertPayloadReasonCode(Payload, 'PRICE'); end; [Test] - procedure RefusalWithoutReasonSendsStatusWithoutReasonElements() + procedure RefusalWithoutReasonCreatesStatusWithoutReasonElements() var EDocument: Record "E-Document"; + FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO] A buyer can refuse an invoice without providing a reason @@ -258,39 +255,12 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The invoice is refused without a reason code or description FREInvoiceMessageMgt.RefuseInvoice(EDocument, '', ''); - SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Refused); - // [THEN] The refusal status is sent without empty reason elements - AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '210'); - AssertPayloadHasNoReason(MessageSenderMock.GetLastPayload()); - end; - - [Test] - procedure MessageSenderMustReportSuccess() - var - EDocument: Record "E-Document"; - EDocumentMessageAPI: Codeunit "E-Document Message API"; - TempBlob: Codeunit "Temp Blob"; - OutStream: OutStream; - MessageEntryNo: Integer; - begin - // [FEATURE] [AI test] - // [SCENARIO] Sending a message without the connector reporting success raises an error - Initialize(); - - // [GIVEN] An incoming E-Document with a lifecycle message and a mock connector that does not report success - CreateIncomingEDocument(EDocument); - TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8); - OutStream.WriteText(''); - MessageEntryNo := EDocumentMessageAPI.CreateMessage( - EDocument, "E-Document Message Type"::"FR Invoice Lifecycle", "E-Doc. Response Type"::Refused, TempBlob); - MessageSenderMock.SetReportSuccess(false); - - // [WHEN] The message is sent - asserterror EDocumentMessageAPI.SendMessage(MessageEntryNo); - - // [THEN] The connector is invoked before its missing success result is rejected - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'The connector must be invoked before its missing success result is rejected.'); + // [THEN] The refusal payload has status 210 without empty reason elements + FindMessage(FREInvoiceMessage, EDocument, "FR E-Invoice Message Type"::Refused); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); + AssertPayloadStatus(Payload, '210'); + AssertPayloadHasNoReason(Payload); end; [Test] @@ -300,7 +270,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO] Processing the same payment application twice is idempotent @@ -310,8 +279,8 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment application is processed twice - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] Only one payment occurrence and one Collected message exist; EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -329,7 +298,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocumentService: Record "E-Document Service"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] A Collected message retains the sender-platform identity captured from its service @@ -339,7 +307,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment is processed and the service identity is subsequently changed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); EDocumentService.Get(EDocument.Service); EDocumentService."FR Sender Platform ID" := 'CHANGED-PLATFORM'; EDocumentService."FR Sender Platform Scheme" := '9999'; @@ -366,7 +334,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO 647421] A Collected message emits the complete PPF platform context @@ -375,15 +343,15 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An eligible payment and a French service with sender-platform identity CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - // [WHEN] The payment is processed and its lifecycle message is sent - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + // [WHEN] The payment is processed and its lifecycle payload is built + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); FREInvoiceMessage.FindFirst(); - SendMessage(FREInvoiceMessage); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); // [THEN] The payload contains the PPF profile, sender, issuer, recipient, and invoice dates - AssertPayloadPPFContext(MessageSenderMock.GetLastPayload(), EDocument); + AssertPayloadPPFContext(Payload, EDocument); end; [Test] @@ -392,7 +360,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] A Collected message supports a service without optional sender-platform identity @@ -402,7 +369,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenarioWithoutSenderPlatform(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The message is created with no frozen platform identity FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -417,7 +384,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO 647421] A Collected message without platform identity retains the CDV profile @@ -426,25 +393,26 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An eligible payment whose service has no sender-platform identity CreatePaymentScenarioWithoutSenderPlatform(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - // [WHEN] The payment is processed and its lifecycle message is sent - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + // [WHEN] The payment is processed and its lifecycle payload is built + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); FREInvoiceMessage.FindFirst(); - SendMessage(FREInvoiceMessage); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); // [THEN] The payload uses the CDV profile and does not contain PPF trade parties - AssertPayloadCDVContext(MessageSenderMock.GetLastPayload()); + AssertPayloadCDVContext(Payload); end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure SingleRateFullPaymentCreatesOneVATRow() var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO 647421] Single-rate full payment creates one frozen VAT row summing to reportable amount @@ -454,7 +422,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] One VAT row is created with amount equal to the message amount and the XML includes amount and rate FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -466,18 +434,18 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.AreEqual(FREInvoiceMessage.Amount, FREInvoiceMessageVAT.Amount, 'VAT row amount must equal message amount.'); Assert.AreEqual(20, FREInvoiceMessageVAT."VAT %", 'VAT rate must match the posting setup.'); Assert.AreEqual('S', Format(FREInvoiceMessageVAT."VAT Category Code"), 'VAT category must be standard.'); - SendMessage(FREInvoiceMessage); - AssertPayloadVATCharacteristic(MessageSenderMock.GetLastPayload(), FREInvoiceMessage.Amount, 20); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); + AssertPayloadVATCharacteristic(Payload, FREInvoiceMessage.Amount, 20); end; [Test] + [TransactionModel(TransactionModel::AutoCommit)] procedure SingleRatePartialPaymentAllocatesPartialAmount() var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] Single-rate partial payment allocates partial amount @@ -487,7 +455,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved, 60); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The message amount and VAT row reflect the partial payment FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -507,7 +475,6 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] Mixed invoice with unrealized and realized VAT reports only proportional eligible share @@ -515,9 +482,9 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An invoice with one unrealized-VAT line (20%, gross 120) and one realized-VAT line (10%, gross 110), full payment of 230 CreateMixedVATPaymentScenario(EDocument, DetailedCustLedgEntry); - + Commit(); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The message amount reflects only the eligible gross share FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -537,7 +504,6 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; VATRowSum: Decimal; begin // [FEATURE] [AI test] @@ -546,9 +512,9 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] An invoice with three unrealized VAT rates (10%, 20%, 7%) and a partial payment of 99 causing rounding residue CreateMultiRatePaymentScenario(EDocument, DetailedCustLedgEntry); - + Commit(); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] The sum of VAT rows equals the message amount deterministically FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -570,7 +536,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] Invoice without unrealized VAT keeps generic payment occurrence but creates no FR Collected message @@ -580,7 +545,7 @@ codeunit 148151 "FR E-Invoice Message Tests" CreateNormalVATPaymentScenario(EDocument, DetailedCustLedgEntry); // [WHEN] The payment application is processed - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] A generic payment occurrence exists but no Collected message is created EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -605,7 +570,6 @@ codeunit 148151 "FR E-Invoice Message Tests" NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; SalesInvoiceLine: Record "Sales Invoice Line"; VATPostingSetup: Record "VAT Posting Setup"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] Reversal copies original frozen rows with negated values even if VAT setup changes after original @@ -613,11 +577,10 @@ codeunit 148151 "FR E-Invoice Message Tests" // [GIVEN] A collected message with a frozen VAT breakdown CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); CollectedMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); CollectedMessage.SetRange(Type, CollectedMessage.Type::Collected); CollectedMessage.FindFirst(); - SendMessage(CollectedMessage); OriginalVAT.SetRange("Message Entry No.", CollectedMessage."Entry No."); OriginalVAT.FindFirst(); @@ -639,7 +602,7 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The payment is unapplied CreateDetailedLedgerEntry(NewDetailedCustLedgEntry, DetailedCustLedgEntry."Cust. Ledger Entry No.", DetailedCustLedgEntry."Applied Cust. Ledger Entry No.", -120); - FREInvoiceMessageMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + ProcessPaymentUnapplication(EDocument, DetailedCustLedgEntry, NewDetailedCustLedgEntry); // [THEN] The reversal has the original frozen rate and negated amount NegativeMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -670,7 +633,6 @@ codeunit 148151 "FR E-Invoice Message Tests" FREInvoiceMessage: Record "FR E-Invoice Message"; FREInvoiceMessageVAT: Record "FR E-Invoice Message VAT"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin // [FEATURE] [AI test] // [SCENARIO 647421] Replay is idempotent and does not duplicate allocation rows @@ -680,8 +642,8 @@ codeunit 148151 "FR E-Invoice Message Tests" CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); // [WHEN] The payment application is processed twice - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); - FREInvoiceMessageMgt.ProcessApplication(DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); + ProcessPaymentApplication(EDocument, DetailedCustLedgEntry); // [THEN] Only one message and one VAT row exist FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); @@ -755,11 +717,13 @@ codeunit 148151 "FR E-Invoice Message Tests" end; [Test] - procedure BuyerAcceptanceQueuesAndSendsStatus205() + procedure BuyerAcceptanceQueuesStatus205() var EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message"; + EDocumentMessageAPI: Codeunit "E-Document Message API"; FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + Payload: Text; begin // [FEATURE] [AI test] // [SCENARIO] Accepting an invoice queues an Accepted message with status 205 @@ -771,15 +735,14 @@ codeunit 148151 "FR E-Invoice Message Tests" // [WHEN] The invoice is accepted FREInvoiceMessageMgt.AcceptInvoice(EDocument); - // [THEN] An Accepted message with status 205 is queued and can be sent + // [THEN] An Accepted message with status 205 is queued FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Accepted); Assert.RecordCount(FREInvoiceMessage, 1); - Assert.AreEqual(0, MessageSenderMock.GetSendCount(), 'Acceptance must queue the message without invoking the connector.'); - SendFirstMessage(EDocument, "FR E-Invoice Message Type"::Accepted); - Assert.AreEqual(1, MessageSenderMock.GetSendCount(), 'One Accepted message must be sent.'); - Assert.AreEqual("E-Doc. Response Type"::Accepted, MessageSenderMock.GetLastResponseType(), 'The child message must be Accepted.'); - AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '205'); + FREInvoiceMessage.FindFirst(); + Assert.AreEqual("E-Doc. Response Type"::Accepted, EDocumentMessageAPI.GetMessageResponseType(FREInvoiceMessage."E-Document Message Entry No."), 'The child message must be Accepted.'); + Payload := BuildMessagePayload(EDocument, FREInvoiceMessage); + AssertPayloadStatus(Payload, '205'); end; [Test] @@ -1507,21 +1470,26 @@ codeunit 148151 "FR E-Invoice Message Tests" exit('urn.cpro.gouv.fr:1p0:CDV:invoice'); end; - local procedure SendFirstMessage(EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type") - var - FREInvoiceMessage: Record "FR E-Invoice Message"; + local procedure FindMessage(var FREInvoiceMessage: Record "FR E-Invoice Message"; EDocument: Record "E-Document"; MessageType: Enum "FR E-Invoice Message Type") begin FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, MessageType); FREInvoiceMessage.FindFirst(); - SendMessage(FREInvoiceMessage); end; - local procedure SendMessage(FREInvoiceMessage: Record "FR E-Invoice Message") + local procedure BuildMessagePayload(EDocument: Record "E-Document"; FREInvoiceMessage: Record "FR E-Invoice Message") Payload: Text var - EDocumentMessageAPI: Codeunit "E-Document Message API"; - begin - EDocumentMessageAPI.SendMessage(FREInvoiceMessage."E-Document Message Entry No."); + FREInvoiceMessageBuilder: Codeunit "FR E-Invoice Message Builder"; + TempBlob: Codeunit "Temp Blob"; + InStream: InStream; + PayloadLine: Text; + begin + FREInvoiceMessageBuilder.BuildMessage(EDocument, FREInvoiceMessage, TempBlob); + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + while not InStream.EOS do begin + InStream.ReadText(PayloadLine); + Payload += PayloadLine; + end; end; local procedure AssertPayloadAmount(Payload: Text; ExpectedAmount: Decimal; ExpectedCurrencyCode: Code[10]) @@ -1651,7 +1619,6 @@ codeunit 148151 "FR E-Invoice Message Tests" begin EDocPaymentOccurrence.DeleteAll(); FREInvoiceMessage.DeleteAll(); - MessageSenderMock.Reset(); EnsureService(); EnsureCompanyInformation(); GeneralLedgerSetup.Get(); @@ -1675,13 +1642,12 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocumentService: Record "E-Document Service"; begin - if not EDocumentService.Get('FR-MESSAGE-MOCK') then begin + if not EDocumentService.Get('FR-MESSAGE') then begin EDocumentService.Init(); - EDocumentService.Code := 'FR-MESSAGE-MOCK'; - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService."Service Integration V2" := EDocumentService."Service Integration V2"::"FR Message Mock"; + EDocumentService.Code := 'FR-MESSAGE'; EDocumentService.Insert(); end; + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; EDocumentService."FR Sender Platform ID" := 'TEST-PLATFORM'; EDocumentService."FR Sender Platform Scheme" := '0238'; EDocumentService."FR Sender Platform Name" := 'Test Platform'; @@ -1694,7 +1660,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(EDocument."Document No.")); EDocument.Direction := EDocument.Direction::Incoming; EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Service := 'FR-MESSAGE'; EDocument.Insert(); end; @@ -1704,7 +1670,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Document No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(EDocument."Document No.")); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Service := 'FR-MESSAGE'; EDocument.Insert(); end; @@ -1717,7 +1683,7 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocumentService: Record "E-Document Service"; begin - EDocumentService.Get('FR-MESSAGE-MOCK'); + EDocumentService.Get('FR-MESSAGE'); Clear(EDocumentService."FR Sender Platform ID"); EDocumentService.Modify(); CreatePaymentScenario(EDocument, DetailedCustLedgEntry, ServiceStatus); @@ -1726,40 +1692,67 @@ codeunit 148151 "FR E-Invoice Message Tests" local procedure CreatePaymentScenarioWithAmount(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; ServiceStatus: Enum "E-Document Service Status"; PaymentAmount: Decimal) var Customer: Record Customer; - EDocumentService: Record "E-Document Service"; - GenJournalBatch: Record "Gen. Journal Batch"; - GenJournalLine: Record "Gen. Journal Line"; - SalesHeader: Record "Sales Header"; + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesLine: Record "Sales Line"; + SalesInvoiceLine: Record "Sales Invoice Line"; + VATEntry: Record "VAT Entry"; VATPostingSetup: Record "VAT Posting Setup"; PostedInvoiceNo: Code[20]; begin - EDocumentService.Get('FR-MESSAGE-MOCK'); - Clear(EDocumentService."Document Format"); - EDocumentService.Modify(); LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); VATPostingSetup."Unrealized VAT Type" := VATPostingSetup."Unrealized VAT Type"::Percentage; VATPostingSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); VATPostingSetup."Tax Category" := 'S'; VATPostingSetup.Modify(true); LibrarySales.CreateCustomer(Customer); - Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); - PrepareCustomerForPosting(Customer); - Customer.Modify(true); - - LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); - PrepareSalesHeaderForPosting(SalesHeader); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + PostedInvoiceNo := CopyStr(Format(CreateGuid()), 1, MaxStrLen(PostedInvoiceNo)); + SalesInvoiceHeader.Init(); + SalesInvoiceHeader."No." := PostedInvoiceNo; + SalesInvoiceHeader."Bill-to Customer No." := Customer."No."; + SalesInvoiceHeader."Posting Date" := WorkDate(); + SalesInvoiceHeader."Document Date" := WorkDate(); + SalesInvoiceHeader.Insert(); + SalesInvoiceLine.Init(); + SalesInvoiceLine."Document No." := PostedInvoiceNo; + SalesInvoiceLine."Line No." := 10000; + SalesInvoiceLine."VAT Bus. Posting Group" := VATPostingSetup."VAT Bus. Posting Group"; + SalesInvoiceLine."VAT Prod. Posting Group" := VATPostingSetup."VAT Prod. Posting Group"; + SalesInvoiceLine.Insert(); + + InvoiceCustLedgerEntry.Init(); + InvoiceCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + InvoiceCustLedgerEntry."Customer No." := Customer."No."; + InvoiceCustLedgerEntry."Posting Date" := WorkDate(); + InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; + InvoiceCustLedgerEntry."Document No." := PostedInvoiceNo; + InvoiceCustLedgerEntry."Transaction No." := InvoiceCustLedgerEntry."Entry No."; + InvoiceCustLedgerEntry.Insert(); + PaymentCustLedgerEntry.Init(); + PaymentCustLedgerEntry."Entry No." := GetNextCustLedgerEntryNo(); + PaymentCustLedgerEntry."Customer No." := Customer."No."; + PaymentCustLedgerEntry."Posting Date" := WorkDate(); + PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; + PaymentCustLedgerEntry."Document No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(PaymentCustLedgerEntry."Document No.")); + PaymentCustLedgerEntry.Insert(); + CreateDetailedLedgerEntry(DetailedCustLedgEntry, InvoiceCustLedgerEntry."Entry No.", PaymentCustLedgerEntry."Entry No.", -PaymentAmount); + + VATEntry.Init(); + VATEntry."Entry No." := GetNextVATEntryNo(); + VATEntry.Type := VATEntry.Type::Sale; + VATEntry."Document Type" := VATEntry."Document Type"::Invoice; + VATEntry."Document No." := PostedInvoiceNo; + VATEntry."Posting Date" := WorkDate(); + VATEntry."Transaction No." := InvoiceCustLedgerEntry."Transaction No."; + VATEntry."VAT Bus. Posting Group" := VATPostingSetup."VAT Bus. Posting Group"; + VATEntry."VAT Prod. Posting Group" := VATPostingSetup."VAT Prod. Posting Group"; + VATEntry."VAT Calculation Type" := VATEntry."VAT Calculation Type"::"Normal VAT"; + VATEntry.Base := -100; + VATEntry.Amount := -20; + VATEntry."Unrealized Base" := -100; + VATEntry."Unrealized Amount" := -20; + VATEntry.Insert(); - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService.Modify(); - - SalesInvoiceHeader.Get(PostedInvoiceNo); EDocument.Init(); EDocument."Document No." := PostedInvoiceNo; EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; @@ -1768,253 +1761,74 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocument."Clearance Date" := CurrentDateTime(); EDocument.Direction := EDocument.Direction::Outgoing; EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; + EDocument.Service := 'FR-MESSAGE'; EDocument.Insert(); CreateServiceStatus(EDocument, ServiceStatus); - - LibraryERM.SelectGenJnlBatch(GenJournalBatch); - LibraryERM.ClearGenJournalLines(GenJournalBatch); - LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, - GenJournalBatch."Journal Template Name", GenJournalBatch.Name, - GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", - GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -PaymentAmount); - GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); - GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); - GenJournalLine.Modify(true); - LibraryERM.PostGeneralJnlLine(GenJournalLine); - - FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); end; local procedure CreateMixedVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") - var - Customer: Record Customer; - EDocumentService: Record "E-Document Service"; - GenJournalBatch: Record "Gen. Journal Batch"; - GenJournalLine: Record "Gen. Journal Line"; - SalesHeader: Record "Sales Header"; - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesLine: Record "Sales Line"; - UnrealizedVATSetup: Record "VAT Posting Setup"; - NormalVATSetup: Record "VAT Posting Setup"; - PostedInvoiceNo: Code[20]; begin - EDocumentService.Get('FR-MESSAGE-MOCK'); - Clear(EDocumentService."Document Format"); - EDocumentService.Modify(); - LibraryERM.CreateVATPostingSetupWithAccounts(UnrealizedVATSetup, UnrealizedVATSetup."VAT Calculation Type"::"Normal VAT", 20); - UnrealizedVATSetup."Unrealized VAT Type" := UnrealizedVATSetup."Unrealized VAT Type"::Percentage; - UnrealizedVATSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); - UnrealizedVATSetup."Tax Category" := 'S'; - UnrealizedVATSetup.Modify(true); - LibraryERM.CreateVATPostingSetupWithAccounts(NormalVATSetup, NormalVATSetup."VAT Calculation Type"::"Normal VAT", 10); - NormalVATSetup.Rename(UnrealizedVATSetup."VAT Bus. Posting Group", NormalVATSetup."VAT Prod. Posting Group"); - LibrarySales.CreateCustomer(Customer); - Customer.Validate("VAT Bus. Posting Group", UnrealizedVATSetup."VAT Bus. Posting Group"); - PrepareCustomerForPosting(Customer); - Customer.Modify(true); - - LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); - PrepareSalesHeaderForPosting(SalesHeader); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(UnrealizedVATSetup, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(NormalVATSetup, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); - - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService.Modify(); - SalesInvoiceHeader.Get(PostedInvoiceNo); - EDocument.Init(); - EDocument."Document No." := PostedInvoiceNo; - EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; - EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; - EDocument."Document Date" := SalesInvoiceHeader."Document Date"; - EDocument."Clearance Date" := CurrentDateTime(); - EDocument.Direction := EDocument.Direction::Outgoing; - EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; - EDocument.Insert(); - CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); - - LibraryERM.SelectGenJnlBatch(GenJournalBatch); - LibraryERM.ClearGenJournalLines(GenJournalBatch); - LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, - GenJournalBatch."Journal Template Name", GenJournalBatch.Name, - GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", - GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -230); - GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); - GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); - GenJournalLine.Modify(true); - LibraryERM.PostGeneralJnlLine(GenJournalLine); - FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved, 230); + CreateVATEntryForScenario(EDocument, 10, false); end; local procedure CreateMultiRatePaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") - var - Customer: Record Customer; - EDocumentService: Record "E-Document Service"; - GenJournalBatch: Record "Gen. Journal Batch"; - GenJournalLine: Record "Gen. Journal Line"; - SalesHeader: Record "Sales Header"; - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesLine: Record "Sales Line"; - VATSetup10: Record "VAT Posting Setup"; - VATSetup20: Record "VAT Posting Setup"; - VATSetup7: Record "VAT Posting Setup"; - PostedInvoiceNo: Code[20]; begin - EDocumentService.Get('FR-MESSAGE-MOCK'); - Clear(EDocumentService."Document Format"); - EDocumentService.Modify(); - LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup20, VATSetup20."VAT Calculation Type"::"Normal VAT", 20); - VATSetup20."Unrealized VAT Type" := VATSetup20."Unrealized VAT Type"::Percentage; - VATSetup20."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); - VATSetup20."Tax Category" := 'S'; - VATSetup20.Modify(true); - LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup10, VATSetup10."VAT Calculation Type"::"Normal VAT", 10); - VATSetup10.Rename(VATSetup20."VAT Bus. Posting Group", VATSetup10."VAT Prod. Posting Group"); - VATSetup10."Unrealized VAT Type" := VATSetup10."Unrealized VAT Type"::Percentage; - VATSetup10."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); - VATSetup10."Tax Category" := 'S'; - VATSetup10.Modify(true); - LibraryERM.CreateVATPostingSetupWithAccounts(VATSetup7, VATSetup7."VAT Calculation Type"::"Normal VAT", 7); - VATSetup7.Rename(VATSetup20."VAT Bus. Posting Group", VATSetup7."VAT Prod. Posting Group"); - VATSetup7."Unrealized VAT Type" := VATSetup7."Unrealized VAT Type"::Percentage; - VATSetup7."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); - VATSetup7."Tax Category" := 'S'; - VATSetup7.Modify(true); - LibrarySales.CreateCustomer(Customer); - Customer.Validate("VAT Bus. Posting Group", VATSetup20."VAT Bus. Posting Group"); - PrepareCustomerForPosting(Customer); - Customer.Modify(true); - - LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); - PrepareSalesHeaderForPosting(SalesHeader); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup20, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup10, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(VATSetup7, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); - - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService.Modify(); - SalesInvoiceHeader.Get(PostedInvoiceNo); - EDocument.Init(); - EDocument."Document No." := PostedInvoiceNo; - EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; - EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; - EDocument."Document Date" := SalesInvoiceHeader."Document Date"; - EDocument."Clearance Date" := CurrentDateTime(); - EDocument.Direction := EDocument.Direction::Outgoing; - EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; - EDocument.Insert(); - CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); - - LibraryERM.SelectGenJnlBatch(GenJournalBatch); - LibraryERM.ClearGenJournalLines(GenJournalBatch); - LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, - GenJournalBatch."Journal Template Name", GenJournalBatch.Name, - GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", - GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -99); - GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); - GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); - GenJournalLine.Modify(true); - LibraryERM.PostGeneralJnlLine(GenJournalLine); - FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved, 99); + CreateVATEntryForScenario(EDocument, 10, true); + CreateVATEntryForScenario(EDocument, 7, true); end; - local procedure CreateNormalVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + local procedure CreateVATEntryForScenario(EDocument: Record "E-Document"; VATPercent: Decimal; UnrealizedVAT: Boolean) var - Customer: Record Customer; - EDocumentService: Record "E-Document Service"; - GenJournalBatch: Record "Gen. Journal Batch"; - GenJournalLine: Record "Gen. Journal Line"; - SalesHeader: Record "Sales Header"; - SalesInvoiceHeader: Record "Sales Invoice Header"; - SalesLine: Record "Sales Line"; + CustLedgerEntry: Record "Cust. Ledger Entry"; + SalesInvoiceLine: Record "Sales Invoice Line"; + VATEntry: Record "VAT Entry"; VATPostingSetup: Record "VAT Posting Setup"; - PostedInvoiceNo: Code[20]; begin - EDocumentService.Get('FR-MESSAGE-MOCK'); - Clear(EDocumentService."Document Format"); - EDocumentService.Modify(); - LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 10); - LibrarySales.CreateCustomer(Customer); - Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); - PrepareCustomerForPosting(Customer); - Customer.Modify(true); - - LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); - PrepareSalesHeaderForPosting(SalesHeader); - LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::"G/L Account", - LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); - SalesLine.Validate("Unit Price", 100); - SalesLine.Modify(true); - PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); - - EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; - EDocumentService.Modify(); - SalesInvoiceHeader.Get(PostedInvoiceNo); - EDocument.Init(); - EDocument."Document No." := PostedInvoiceNo; - EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; - EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; - EDocument."Document Date" := SalesInvoiceHeader."Document Date"; - EDocument."Clearance Date" := CurrentDateTime(); - EDocument.Direction := EDocument.Direction::Outgoing; - EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; - EDocument.Service := 'FR-MESSAGE-MOCK'; - EDocument.Insert(); - CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); - - LibraryERM.SelectGenJnlBatch(GenJournalBatch); - LibraryERM.ClearGenJournalLines(GenJournalBatch); - LibraryERM.CreateGeneralJnlLineWithBalAcc(GenJournalLine, - GenJournalBatch."Journal Template Name", GenJournalBatch.Name, - GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", - GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -110); - GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); - GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); - GenJournalLine.Modify(true); - LibraryERM.PostGeneralJnlLine(GenJournalLine); - FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); - end; + SalesInvoiceLine.SetRange("Document No.", EDocument."Document No."); + SalesInvoiceLine.FindFirst(); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", VATPercent); + VATPostingSetup.Rename(SalesInvoiceLine."VAT Bus. Posting Group", VATPostingSetup."VAT Prod. Posting Group"); + if UnrealizedVAT then begin + VATPostingSetup."Unrealized VAT Type" := VATPostingSetup."Unrealized VAT Type"::Percentage; + VATPostingSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + end; + VATPostingSetup."Tax Category" := 'S'; + VATPostingSetup.Modify(true); - local procedure PrepareCustomerForPosting(var Customer: Record Customer) - begin - Customer.Validate(Address, 'Test Address'); - Customer.Validate(City, 'Paris'); - Customer.Validate("Post Code", '75001'); - Customer.Validate("Country/Region Code", 'FR'); - Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + CustLedgerEntry.SetRange("Document No.", EDocument."Document No."); + CustLedgerEntry.FindFirst(); + VATEntry.Init(); + VATEntry."Entry No." := GetNextVATEntryNo(); + VATEntry.Type := VATEntry.Type::Sale; + VATEntry."Document Type" := VATEntry."Document Type"::Invoice; + VATEntry."Document No." := EDocument."Document No."; + VATEntry."Posting Date" := EDocument."Posting Date"; + VATEntry."Transaction No." := CustLedgerEntry."Transaction No."; + VATEntry."VAT Bus. Posting Group" := VATPostingSetup."VAT Bus. Posting Group"; + VATEntry."VAT Prod. Posting Group" := VATPostingSetup."VAT Prod. Posting Group"; + VATEntry."VAT Calculation Type" := VATEntry."VAT Calculation Type"::"Normal VAT"; + VATEntry.Base := -100; + VATEntry.Amount := -VATPercent; + if UnrealizedVAT then begin + VATEntry."Unrealized Base" := VATEntry.Base; + VATEntry."Unrealized Amount" := VATEntry.Amount; + end; + VATEntry.Insert(); end; - local procedure PrepareSalesHeaderForPosting(var SalesHeader: Record "Sales Header") + local procedure CreateNormalVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + VATEntry: Record "VAT Entry"; begin - SalesHeader.Validate("Bill-to Address", 'Test Address'); - SalesHeader.Validate("Bill-to City", 'Paris'); - SalesHeader.Validate("Bill-to Post Code", '75001'); - SalesHeader.Validate("Bill-to Country/Region Code", 'FR'); - SalesHeader.Validate("Ship-to Address", 'Test Address'); - SalesHeader.Validate("Ship-to City", 'Paris'); - SalesHeader.Validate("Ship-to Post Code", '75001'); - SalesHeader.Validate("Ship-to Country/Region Code", 'FR'); - SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); - SalesHeader.Modify(true); + CreatePaymentScenario(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved); + VATEntry.SetRange("Document No.", EDocument."Document No."); + VATEntry.FindFirst(); + VATEntry."Unrealized Base" := 0; + VATEntry."Unrealized Amount" := 0; + VATEntry.Modify(); end; local procedure CreateServiceStatus(EDocument: Record "E-Document"; ServiceStatus: Enum "E-Document Service Status") @@ -2042,18 +1856,36 @@ codeunit 148151 "FR E-Invoice Message Tests" DetailedCustLedgEntry.Insert(); end; - local procedure FindApplicationDetailedEntry(var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; InvoiceDocNo: Code[20]) + local procedure ProcessPaymentApplication(EDocument: Record "E-Document"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") var - CustLedgerEntry: Record "Cust. Ledger Entry"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin - CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); - CustLedgerEntry.SetRange("Document No.", InvoiceDocNo); - CustLedgerEntry.FindFirst(); - DetailedCustLedgEntry.SetRange("Cust. Ledger Entry No.", CustLedgerEntry."Entry No."); - DetailedCustLedgEntry.SetRange("Entry Type", DetailedCustLedgEntry."Entry Type"::Application); - DetailedCustLedgEntry.SetRange("Initial Document Type", DetailedCustLedgEntry."Initial Document Type"::Invoice); - DetailedCustLedgEntry.SetFilter(Amount, '<%1', 0); - DetailedCustLedgEntry.FindLast(); + EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); + Commit(); + + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + EDocPaymentOccurrence.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); + EDocPaymentOccurrence.FindFirst(); + FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + end; + + local procedure ProcessPaymentUnapplication(EDocument: Record "E-Document"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + begin + EDocPaymentOccurrenceMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); + Commit(); + + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Reversed); + EDocPaymentOccurrence.SetRange("Source Occurrence ID", NewDetailedCustLedgEntry.SystemId); + EDocPaymentOccurrence.FindFirst(); + FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); end; local procedure GetNextDetailedLedgerEntryNo(): Integer @@ -2064,4 +1896,23 @@ codeunit 148151 "FR E-Invoice Message Tests" exit(DetailedCustLedgEntry."Entry No." + 1); exit(1); end; + + local procedure GetNextCustLedgerEntryNo(): Integer + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetFilter("Entry No.", '<0'); + if CustLedgerEntry.FindFirst() then + exit(CustLedgerEntry."Entry No." - 1); + exit(-1); + end; + + local procedure GetNextVATEntryNo(): Integer + var + VATEntry: Record "VAT Entry"; + begin + if VATEntry.FindLast() then + exit(VATEntry."Entry No." + 1); + exit(1); + end; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 4c10af7bc5d..9aae35cc616 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -27,6 +27,11 @@ codeunit 6133 "E-Document Background Jobs" ScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0); end; + procedure SchedulePaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + ScheduleEDocumentJob(Codeunit::"E-Doc. Payment Occurrence Mgt.", EDocPaymentOccurrence.RecordId(), 0); + end; + procedure TryScheduleMessageSend(EDocumentMessage: Record "E-Document Message"): Boolean begin exit(TryScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0)); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 503d87a3790..dadf82705d8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -11,6 +11,7 @@ using Microsoft.Finance.ReceivablesPayables; using Microsoft.Sales.Customer; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using System.Threading; /// /// Captures payment applications and reversals for outgoing E-Documents and publishes them to localization apps. @@ -18,6 +19,7 @@ using Microsoft.Sales.Receivables; codeunit 6536 "E-Doc. Payment Occurrence Mgt." { Access = Public; + TableNo = "Job Queue Entry"; InherentEntitlements = X; InherentPermissions = X; @@ -29,66 +31,23 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." tabledata "Sales Invoice Header" = r; trigger OnRun() + var + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; begin - if ProcessUnapplicationCall then begin - ProcessUnapplication(OldDetailedCustLedgEntry, DetailedCustLedgEntry); - exit; - end; - - ProcessApplication(DetailedCustLedgEntry); + EDocPaymentOccurrence.Get(Rec."Record ID to Process"); + OnAfterCreatePaymentOccurrence(EDocPaymentOccurrence); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] local procedure OnAfterInsertDtldCustLedgEntry(var DtldCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; GenJournalLine: Record "Gen. Journal Line"; DtldCVLedgEntryBuffer: Record "Detailed CV Ledg. Entry Buffer"; Offset: Integer) begin - if not TryProcessApplication(DtldCustLedgEntry) then begin - LogProcessingFailure(DtldCustLedgEntry, ApplicationTelemetryLbl); - ClearLastError(); - end; + ProcessApplication(DtldCustLedgEntry); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntryUnapply', '', false, false)] local procedure OnAfterInsertDtldCustLedgEntryUnapply(var CustomerPostingGroup: Record "Customer Posting Group"; var OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; var GenJnlLine: Record "Gen. Journal Line"; var NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") begin - if not TryProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry) then begin - LogProcessingFailure(NewDetailedCustLedgEntry, UnapplicationTelemetryLbl); - ClearLastError(); - end; - end; - - local procedure TryProcessApplication(DetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"): Boolean - var - EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; - begin - EDocPaymentOccurrenceMgt.SetApplicationParameters(DetailedCustLedgEntryParameter); - exit(EDocPaymentOccurrenceMgt.Run()); - end; - - local procedure TryProcessUnapplication(OldDetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"): Boolean - var - EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; - begin - EDocPaymentOccurrenceMgt.SetUnapplicationParameters(OldDetailedCustLedgEntryParameter, NewDetailedCustLedgEntry); - exit(EDocPaymentOccurrenceMgt.Run()); - end; - - internal procedure SetApplicationParameters(DetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry") - begin - DetailedCustLedgEntry := DetailedCustLedgEntryParameter; - end; - - internal procedure SetUnapplicationParameters(OldDetailedCustLedgEntryParameter: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") - begin - ProcessUnapplicationCall := true; - OldDetailedCustLedgEntry := OldDetailedCustLedgEntryParameter; - DetailedCustLedgEntry := NewDetailedCustLedgEntry; - end; - - local procedure LogProcessingFailure(DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; TelemetryMessage: Text) - begin - Session.LogMessage( - '0000N1G', StrSubstNo(TelemetryMessage, DetailedCustLedgEntry."Entry No.", GetLastErrorText()), - Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PaymentOccurrenceTelemetryCategoryTok); + ProcessUnapplication(OldDetailedCustLedgEntry, NewDetailedCustLedgEntry); end; /// @@ -149,6 +108,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." local procedure CreateOccurrence(EDocumentEntryNo: Integer; OccurrenceType: Enum "E-Doc. Payment Occurrence Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocumentEntryNo); EDocPaymentOccurrence.SetRange("Source Occurrence ID", SourceOccurrenceID); @@ -167,7 +127,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence."Detailed Ledger Entry No." := DetailedLedgerEntryNo; EDocPaymentOccurrence."Created At" := CurrentDateTime(); EDocPaymentOccurrence.Insert(); - OnAfterCreatePaymentOccurrence(EDocPaymentOccurrence); + EDocumentBackgroundJobs.SchedulePaymentOccurrence(EDocPaymentOccurrence); end; local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean @@ -202,12 +162,4 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin end; - - var - DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - OldDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - ProcessUnapplicationCall: Boolean; - ApplicationTelemetryLbl: Label 'E-Document payment occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; - UnapplicationTelemetryLbl: Label 'E-Document payment reversal occurrence processing failed for detailed customer ledger entry %1. Error: %2', Locked = true; - PaymentOccurrenceTelemetryCategoryTok: Label 'E-Document', Locked = true; } \ No newline at end of file From 272a9165ed0b86bb608dd31ddc1e42d2641a4405 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 01:00:43 +0200 Subject: [PATCH 40/49] new changes --- .../src/Core/FREInvoiceMessageMgt.Codeunit.al | 24 +-- .../src/FREInvoiceMessageTests.Codeunit.al | 139 +++++++++++++++++- .../EDocCoreObjects.PermissionSet.al | 2 + .../Permissions/EDocCoreUser.PermissionSet.al | 2 + .../EDocumentBackgroundJobs.Codeunit.al | 25 ++++ .../Message/EDocPaymentOccurrence.Table.al | 28 ++++ ...DocPaymentOccurrenceDispatcher.Codeunit.al | 32 ++++ .../EDocPaymentOccurrenceMgt.Codeunit.al | 60 +++++++- .../EDocPaymentOccurrenceRunner.Codeunit.al | 20 +++ .../EDocPaymentOccurrenceStatus.Enum.al | 31 ++++ .../App/src/Setup/EDocumentSetup.Codeunit.al | 4 + .../src/Setup/EDocumentUpgrade.Codeunit.al | 4 + .../Test/src/Mock/EDocImplState.Codeunit.al | 25 ++++ .../EDocMessageMgtTests.Codeunit.al | 106 +++++++++++++ 14 files changed, 469 insertions(+), 33 deletions(-) create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al create mode 100644 src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index 329f8cfa860..eed949d754b 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -19,16 +19,10 @@ codeunit 10975 "FR E-Invoice Message Mgt." Access = Internal; InherentEntitlements = X; InherentPermissions = X; - TableNo = "E-Doc. Payment Occurrence"; Permissions = tabledata "FR E-Invoice Message" = rimd, tabledata "FR E-Invoice Message VAT" = rid; - trigger OnRun() - begin - CreatePaymentLifecycleMessage(Rec); - end; - internal procedure AcceptInvoice(EDocument: Record "E-Document") begin if not ResolveFrenchService(EDocument) then @@ -62,21 +56,7 @@ codeunit 10975 "FR E-Invoice Message Mgt." [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)] local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin - ProcessPaymentOccurrence(EDocPaymentOccurrence); - end; - - internal procedure ProcessPaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") - var - ErrorMessage: Text; - begin - if Codeunit.Run(Codeunit::"FR E-Invoice Message Mgt.", EDocPaymentOccurrence) then - exit; - - ErrorMessage := GetLastErrorText(); - Session.LogMessage( - '0000N1F', StrSubstNo(PaymentLifecycleMessageFailedTelemetryMsg, EDocPaymentOccurrence."Entry No.", EDocPaymentOccurrence."E-Document Entry No.", ErrorMessage), - Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', PaymentLifecycleTelemetryCategoryTok); - ClearLastError(); + CreatePaymentLifecycleMessage(EDocPaymentOccurrence); end; local procedure CreatePaymentLifecycleMessage(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") @@ -546,6 +526,4 @@ codeunit 10975 "FR E-Invoice Message Mgt." VATEntryCurrencyErr: Label 'VAT entry %1 does not contain amounts in lifecycle currency %2.', Comment = '%1 = VAT entry number, %2 = currency code'; OriginalVATBreakdownErr: Label 'The VAT breakdown for original French invoice message %1 does not exist.', Comment = '%1 = French invoice message entry number'; InvalidSIRENErr: Label 'Company registration number %1 cannot be normalized to a nine-digit SIREN.', Comment = '%1 = company registration number'; - PaymentLifecycleMessageFailedTelemetryMsg: Label 'French lifecycle message creation failed for payment occurrence %1 and E-Document %2. Error: %3', Locked = true; - PaymentLifecycleTelemetryCategoryTok: Label 'E-Document', Locked = true; } \ No newline at end of file diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index fdf042e5d77..72cc1d696ec 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -7,11 +7,13 @@ namespace Microsoft.eServices.EDocument.Formats.Test; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Formats; using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; using System.Utilities; @@ -98,6 +100,29 @@ codeunit 148151 "FR E-Invoice Message Tests" Assert.RecordCount(FREInvoiceMessage, 1); end; + [Test] + procedure PostedPaymentCreatesDurableOccurrence() + var + EDocument: Record "E-Document"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + begin + // [FEATURE] [AI test] + // [SCENARIO] Posting an applied customer payment creates a durable payment occurrence + Initialize(); + + // [GIVEN] A posted sales invoice represented by an approved French E-Document + CreatePostedPaymentScenario(EDocument, DetailedCustLedgEntry); + + // [WHEN] The customer payment is posted by the scenario + + // [THEN] Posting succeeds and the subscriber persists one payment occurrence + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); + EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); + Assert.RecordCount(EDocPaymentOccurrence, 1); + end; + [Test] procedure PaymentApplicationForSentDocumentDoesNotCreateCollected() var @@ -133,7 +158,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin // [FEATURE] [AI test] // [SCENARIO] Invalid optional lifecycle configuration does not block a payment and its occurrence can be retried @@ -152,18 +177,22 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); Assert.RecordCount(EDocPaymentOccurrence, 1); + EDocPaymentOccurrence.FindFirst(); + Assert.AreEqual(EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status, 'The failed lifecycle message must remain available for retry.'); FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); Assert.RecordCount(FREInvoiceMessage, 0); // [WHEN] The configuration is repaired and the persisted occurrence is retried EnsureCompanyInformation(); - Commit(); - EDocPaymentOccurrence.FindFirst(); - FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + EDocPaymentOccurrence."Next Attempt At" := 0DT; + EDocPaymentOccurrence.Modify(); + EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); // [THEN] The French lifecycle message is created from the original occurrence Assert.RecordCount(FREInvoiceMessage, 1); + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + Assert.AreEqual(EDocPaymentOccurrence.Status::Processed, EDocPaymentOccurrence.Status, 'The successful retry must mark the occurrence Processed.'); end; [Test] @@ -1766,6 +1795,102 @@ codeunit 148151 "FR E-Invoice Message Tests" CreateServiceStatus(EDocument, ServiceStatus); end; + local procedure CreatePostedPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + GenJournalBatch: Record "Gen. Journal Batch"; + GenJournalLine: Record "Gen. Journal Line"; + SalesHeader: Record "Sales Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + SalesLine: Record "Sales Line"; + VATPostingSetup: Record "VAT Posting Setup"; + PostedInvoiceNo: Code[20]; + begin + EDocumentService.Get('FR-MESSAGE'); + Clear(EDocumentService."Document Format"); + EDocumentService.Modify(); + LibraryERM.CreateVATPostingSetupWithAccounts(VATPostingSetup, VATPostingSetup."VAT Calculation Type"::"Normal VAT", 20); + VATPostingSetup."Unrealized VAT Type" := VATPostingSetup."Unrealized VAT Type"::Percentage; + VATPostingSetup."Sales VAT Unreal. Account" := LibraryERM.CreateGLAccountNo(); + VATPostingSetup."Tax Category" := 'S'; + VATPostingSetup.Modify(true); + LibrarySales.CreateCustomer(Customer); + Customer.Validate("VAT Bus. Posting Group", VATPostingSetup."VAT Bus. Posting Group"); + PrepareCustomerForPosting(Customer); + Customer.Modify(true); + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Invoice, Customer."No."); + PrepareSalesHeaderForPosting(SalesHeader); + LibrarySales.CreateSalesLine( + SalesLine, SalesHeader, SalesLine.Type::"G/L Account", + LibraryERM.CreateGLAccountWithVATPostingSetup(VATPostingSetup, "General Posting Type"::Sale), 1); + SalesLine.Validate("Unit Price", 100); + SalesLine.Modify(true); + PostedInvoiceNo := LibrarySales.PostSalesDocument(SalesHeader, true, true); + EDocumentService."Document Format" := EDocumentService."Document Format"::"Peppol BIS 3.0 FR"; + EDocumentService.Modify(); + SalesInvoiceHeader.Get(PostedInvoiceNo); + EDocument.Init(); + EDocument."Document No." := PostedInvoiceNo; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument."Posting Date" := SalesInvoiceHeader."Posting Date"; + EDocument."Document Date" := SalesInvoiceHeader."Document Date"; + EDocument."Clearance Date" := CurrentDateTime(); + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Service := 'FR-MESSAGE'; + EDocument.Insert(); + CreateServiceStatus(EDocument, "E-Document Service Status"::Approved); + LibraryERM.SelectGenJnlBatch(GenJournalBatch); + LibraryERM.ClearGenJournalLines(GenJournalBatch); + LibraryERM.CreateGeneralJnlLineWithBalAcc( + GenJournalLine, GenJournalBatch."Journal Template Name", GenJournalBatch.Name, + GenJournalLine."Document Type"::Payment, GenJournalLine."Account Type"::Customer, Customer."No.", + GenJournalLine."Account Type"::"G/L Account", LibraryERM.CreateGLAccountNo(), -120); + GenJournalLine.Validate("Applies-to Doc. Type", GenJournalLine."Applies-to Doc. Type"::Invoice); + GenJournalLine.Validate("Applies-to Doc. No.", PostedInvoiceNo); + GenJournalLine.Modify(true); + LibraryERM.PostGeneralJnlLine(GenJournalLine); + FindApplicationDetailedEntry(DetailedCustLedgEntry, PostedInvoiceNo); + end; + + local procedure PrepareCustomerForPosting(var Customer: Record Customer) + begin + Customer.Validate(Address, 'Test Address'); + Customer.Validate(City, 'Paris'); + Customer.Validate("Post Code", '75001'); + Customer.Validate("Country/Region Code", 'FR'); + Customer."VAT Registration No." := LibraryERM.GenerateVATRegistrationNo('FR'); + end; + + local procedure PrepareSalesHeaderForPosting(var SalesHeader: Record "Sales Header") + begin + SalesHeader.Validate("Bill-to Address", 'Test Address'); + SalesHeader.Validate("Bill-to City", 'Paris'); + SalesHeader.Validate("Bill-to Post Code", '75001'); + SalesHeader.Validate("Bill-to Country/Region Code", 'FR'); + SalesHeader.Validate("Ship-to Address", 'Test Address'); + SalesHeader.Validate("Ship-to City", 'Paris'); + SalesHeader.Validate("Ship-to Post Code", '75001'); + SalesHeader.Validate("Ship-to Country/Region Code", 'FR'); + SalesHeader.Validate("Your Reference", 'FR-BUYER-REF'); + SalesHeader.Modify(true); + end; + + local procedure FindApplicationDetailedEntry(var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; InvoiceDocNo: Code[20]) + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + begin + CustLedgerEntry.SetRange("Document Type", CustLedgerEntry."Document Type"::Invoice); + CustLedgerEntry.SetRange("Document No.", InvoiceDocNo); + CustLedgerEntry.FindFirst(); + DetailedCustLedgEntry.SetRange("Cust. Ledger Entry No.", CustLedgerEntry."Entry No."); + DetailedCustLedgEntry.SetRange("Entry Type", DetailedCustLedgEntry."Entry Type"::Application); + DetailedCustLedgEntry.SetRange("Initial Document Type", DetailedCustLedgEntry."Initial Document Type"::Invoice); + DetailedCustLedgEntry.SetFilter(Amount, '<%1', 0); + DetailedCustLedgEntry.FindLast(); + end; + local procedure CreateMixedVATPaymentScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") begin CreatePaymentScenarioWithAmount(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved, 230); @@ -1860,7 +1985,6 @@ codeunit 148151 "FR E-Invoice Message Tests" var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); Commit(); @@ -1869,14 +1993,13 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); EDocPaymentOccurrence.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); EDocPaymentOccurrence.FindFirst(); - FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); end; local procedure ProcessPaymentUnapplication(EDocument: Record "E-Document"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; - FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt."; begin EDocPaymentOccurrenceMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); Commit(); @@ -1885,7 +2008,7 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Reversed); EDocPaymentOccurrence.SetRange("Source Occurrence ID", NewDetailedCustLedgEntry.SystemId); EDocPaymentOccurrence.FindFirst(); - FREInvoiceMessageMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); end; local procedure GetNextDetailedLedgerEntryNo(): Integer diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al index b5c4b2029d1..4e2fdcd725d 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al @@ -109,6 +109,8 @@ permissionset 6100 "E-Doc. Core - Objects" codeunit "E-Doc. Message Context" = X, codeunit "E-Doc. Message Mgt." = X, codeunit "E-Doc. Msg. Transport Default" = X, + codeunit "E-Doc. Payment Occ. Dispatcher" = X, + codeunit "E-Doc. Payment Occ. Runner" = X, codeunit "Service Participant" = X, page "E-Doc. Changes Part" = X, page "E-Doc. Changes Preview" = X, diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al index 218d6c5640b..cddca5e67a5 100644 --- a/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al +++ b/src/Apps/W1/EDocument/App/Permissions/EDocCoreUser.PermissionSet.al @@ -29,6 +29,8 @@ permissionset 6105 "E-Doc. Core - User" codeunit "E-Doc. Message Response Job" = X, codeunit "E-Doc. Message Send Job" = X, codeunit "E-Doc. Message Send Runner" = X, + codeunit "E-Doc. Payment Occ. Dispatcher" = X, + codeunit "E-Doc. Payment Occ. Runner" = X, codeunit "E-Doc. Payment Occurrence Mgt." = X, tabledata "E-Document" = iMD, #region Service diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 9aae35cc616..b7f9503c683 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -32,6 +32,31 @@ codeunit 6133 "E-Document Background Jobs" ScheduleEDocumentJob(Codeunit::"E-Doc. Payment Occurrence Mgt.", EDocPaymentOccurrence.RecordId(), 0); end; + [TryFunction] + procedure TrySchedulePaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + SchedulePaymentOccurrence(EDocPaymentOccurrence); + end; + + [TryFunction] + internal procedure TryEnsurePaymentOccurrenceDispatcher() + var + JobQueueEntry: Record "Job Queue Entry"; + BlankRecordId: RecordId; + begin + JobQueueEntry.SetRange("Object Type to Run", JobQueueEntry."Object Type to Run"::Codeunit); + JobQueueEntry.SetRange("Object ID to Run", Codeunit::"E-Doc. Payment Occ. Dispatcher"); + JobQueueEntry.SetFilter(Status, '<>%1', JobQueueEntry.Status::Finished); + if not JobQueueEntry.IsEmpty() then + exit; + + JobQueueEntry.ScheduleRecurrentJobQueueEntryWithFrequency( + JobQueueEntry."Object Type to Run"::Codeunit, Codeunit::"E-Doc. Payment Occ. Dispatcher", BlankRecordId, 5); + JobQueueEntry."Job Queue Category Code" := JobQueueCategoryTok; + JobQueueEntry."No. of Attempts to Run" := 0; + JobQueueEntry.Modify(); + end; + procedure TryScheduleMessageSend(EDocumentMessage: Record "E-Document Message"): Boolean begin exit(TryScheduleEDocumentJob(Codeunit::"E-Doc. Message Send Job", EDocumentMessage.RecordId(), 0)); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al index 7a143a77010..03e872c8d9b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrence.Table.al @@ -75,6 +75,31 @@ table 6433 "E-Doc. Payment Occurrence" Caption = 'Created At'; DataClassification = SystemMetadata; } + field(11; Status; Enum "E-Doc. Payment Occ. Status") + { + Caption = 'Status'; + DataClassification = SystemMetadata; + } + field(12; "Last Attempt At"; DateTime) + { + Caption = 'Last Attempt At'; + DataClassification = SystemMetadata; + } + field(13; "Retry Count"; Integer) + { + Caption = 'Retry Count'; + DataClassification = SystemMetadata; + } + field(14; "Last Error"; Text[2048]) + { + Caption = 'Last Error'; + DataClassification = CustomerContent; + } + field(15; "Next Attempt At"; DateTime) + { + Caption = 'Next Attempt At'; + DataClassification = SystemMetadata; + } } keys @@ -90,5 +115,8 @@ table 6433 "E-Doc. Payment Occurrence" key(Source; "Source Occurrence ID", Type) { } + key(Processing; Status, "Next Attempt At") + { + } } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al new file mode 100644 index 00000000000..b82f583131d --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.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.eServices.EDocument.Processing.Message; + +codeunit 6435 "E-Doc. Payment Occ. Dispatcher" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + Permissions = tabledata "E-Doc. Payment Occurrence" = rm; + + trigger OnRun() + var + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + EntryNo: Integer; + begin + EDocPaymentOccurrence.SetFilter(Status, '%1|%2|%3', EDocPaymentOccurrence.Status::Pending, EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status::Processing); + EDocPaymentOccurrence.SetFilter("Next Attempt At", '%1|<=%2', 0DT, CurrentDateTime()); + while EDocPaymentOccurrence.FindFirst() do begin + EntryNo := EDocPaymentOccurrence."Entry No."; + Commit(); + if EDocPaymentOccurrence.Get(EntryNo) then + EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + EDocPaymentOccurrence.SetFilter(Status, '%1|%2|%3', EDocPaymentOccurrence.Status::Pending, EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status::Processing); + EDocPaymentOccurrence.SetFilter("Next Attempt At", '%1|<=%2', 0DT, CurrentDateTime()); + end; + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index dadf82705d8..25a4845a459 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -35,7 +35,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; begin EDocPaymentOccurrence.Get(Rec."Record ID to Process"); - OnAfterCreatePaymentOccurrence(EDocPaymentOccurrence); + ProcessPaymentOccurrence(EDocPaymentOccurrence); end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Gen. Jnl.-Post Line", 'OnAfterInsertDtldCustLedgEntry', '', false, false)] @@ -110,6 +110,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin + EDocPaymentOccurrence.LockTable(); EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocumentEntryNo); EDocPaymentOccurrence.SetRange("Source Occurrence ID", SourceOccurrenceID); EDocPaymentOccurrence.SetRange(Type, OccurrenceType); @@ -126,8 +127,60 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence."Event Date" := EventDate; EDocPaymentOccurrence."Detailed Ledger Entry No." := DetailedLedgerEntryNo; EDocPaymentOccurrence."Created At" := CurrentDateTime(); + EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Pending; EDocPaymentOccurrence.Insert(); - EDocumentBackgroundJobs.SchedulePaymentOccurrence(EDocPaymentOccurrence); + if not EDocumentBackgroundJobs.TrySchedulePaymentOccurrence(EDocPaymentOccurrence) then begin + EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Error; + EDocPaymentOccurrence."Retry Count" := 1; + EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(EDocPaymentOccurrence."Last Error")); + EDocPaymentOccurrence.Modify(); + ClearLastError(); + end; + end; + + internal procedure ProcessPaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + var + LastErrorText: Text; + begin + EDocPaymentOccurrence.LockTable(); + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + if (EDocPaymentOccurrence.Status = EDocPaymentOccurrence.Status::Processed) or + ((EDocPaymentOccurrence.Status = EDocPaymentOccurrence.Status::Processing) and + (EDocPaymentOccurrence."Next Attempt At" > CurrentDateTime())) + then + exit; + + EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Processing; + EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence.Modify(); + Commit(); + if Codeunit.Run(Codeunit::"E-Doc. Payment Occ. Runner", EDocPaymentOccurrence) then begin + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Processed; + EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); + EDocPaymentOccurrence."Next Attempt At" := 0DT; + Clear(EDocPaymentOccurrence."Last Error"); + EDocPaymentOccurrence.Modify(); + exit; + end; + + LastErrorText := GetLastErrorText(); + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Error; + EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); + EDocPaymentOccurrence."Retry Count" += 1; + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocPaymentOccurrence."Last Error")); + EDocPaymentOccurrence.Modify(); + ClearLastError(); + end; + + internal procedure NotifyPaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + OnAfterCreatePaymentOccurrence(EDocPaymentOccurrence); end; local procedure FindInvoiceEDocuments(var EDocument: Record "E-Document"; InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"): Boolean @@ -162,4 +215,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin end; + + var + RetryDelay: Duration = 300000; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al new file mode 100644 index 00000000000..ad9f57034fe --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al @@ -0,0 +1,20 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +codeunit 6249 "E-Doc. Payment Occ. Runner" +{ + Access = Internal; + TableNo = "E-Doc. Payment Occurrence"; + InherentEntitlements = X; + InherentPermissions = X; + + trigger OnRun() + var + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + EDocPaymentOccurrenceMgt.NotifyPaymentOccurrence(Rec); + end; +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al new file mode 100644 index 00000000000..56b2d0bae7e --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al @@ -0,0 +1,31 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +/// +/// Operational status of an E-Document payment occurrence. +/// +enum 6539 "E-Doc. Payment Occ. Status" +{ + Access = Public; + Extensible = false; + + value(0; Pending) + { + Caption = 'Pending'; + } + value(1; Processed) + { + Caption = 'Processed'; + } + value(2; Error) + { + Caption = 'Error'; + } + value(3; Processing) + { + Caption = 'Processing'; + } +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al index d64b2b4f844..3b24d41660c 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al @@ -13,9 +13,13 @@ codeunit 6145 "E-Document Setup" Subtype = Install; trigger OnInstallAppPerCompany() + var + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin InstallWorkFlowTableRelation(); AddEDocumentLogToAllowedTables(); + if not EDocumentBackgroundJobs.TryEnsurePaymentOccurrenceDispatcher() then + ClearLastError(); end; local procedure AddEDocumentLogToAllowedTables() diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al index aad2d5d532e..c69eca4dd7b 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al @@ -19,6 +19,8 @@ codeunit 6168 "E-Document Upgrade" InherentEntitlements = X; trigger OnUpgradePerCompany() + var + EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin UpgradeLogURLMaxLength(); #if not CLEAN29 @@ -26,6 +28,8 @@ codeunit 6168 "E-Document Upgrade" #endif UpgradeDataExchV2Defs(); UpgradeEnableVATOptionsForPurchEDoc(); + if not EDocumentBackgroundJobs.TryEnsurePaymentOccurrenceDispatcher() then + ClearLastError(); end; local procedure UpgradeLogURLMaxLength() diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al index ea68ee954f3..37832ae1fcd 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al @@ -21,6 +21,7 @@ codeunit 139630 "E-Doc. Impl. State" PurchDocTestBuffer: Codeunit "E-Doc. Test Buffer"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; EnableOnCheck, DisableOnCreateOutput, DisableOnCreateBatch, IsAsync2, EnableHttpData, ThrowIntegrationRuntimeError, ThrowIntegrationLoggedError : Boolean; + ThrowPaymentOccurrenceProcessingError, ThrowPaymentOccurrenceSchedulingError: Boolean; EnableSourceDocumentHeaderCapture: Boolean; ThrowRuntimeError, ThrowLoggedError, ThrowBasicInfoError, ThrowCompleteInfoError, OnGetResponseSuccess, ActionHasUpdate : Boolean; #if not CLEAN26 @@ -35,6 +36,20 @@ codeunit 139630 "E-Doc. Impl. State" LibraryVariableStorage.Enqueue(EDocument); end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Document Background Jobs", 'OnBeforeScheduleEDocumentJob', '', false, false)] + local procedure OnBeforeScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId) + begin + if ThrowPaymentOccurrenceSchedulingError and (CodeunitId = Codeunit::"E-Doc. Payment Occurrence Mgt.") then + Error('TEST PAYMENT OCCURRENCE SCHEDULING'); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)] + local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + begin + if ThrowPaymentOccurrenceProcessingError then + Error('TEST PAYMENT OCCURRENCE PROCESSING'); + end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Export", 'OnBeforeCreateEDocument', '', false, false)] local procedure OnBeforeCreatedEDocument(var EDocument: Record "E-Document") begin @@ -490,6 +505,16 @@ codeunit 139630 "E-Doc. Impl. State" ThrowIntegrationRuntimeError := true; end; + internal procedure SetThrowPaymentOccurrenceProcessingError() + begin + ThrowPaymentOccurrenceProcessingError := true; + end; + + internal procedure SetThrowPaymentOccurrenceSchedulingError() + begin + ThrowPaymentOccurrenceSchedulingError := true; + end; + internal procedure SetHttpResponse(HttpResponse: HttpResponseMessage) begin LocalHttpResponse := HttpResponse; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 87fe8a9234c..38f512a4ca3 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -8,6 +8,8 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Integration; using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Sales.Customer; +using Microsoft.Sales.History; +using Microsoft.Sales.Receivables; using System.Threading; using System.Utilities; @@ -355,6 +357,74 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" Assert.RecordCount(JobQueueEntry, 0); end; + [Test] + procedure PaymentOccurrenceSchedulingFailureDoesNotEscapeCapture() + var + Customer: Record Customer; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocument: Record "E-Document"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A background-job scheduling failure does not fail payment occurrence capture + Initialize(Customer); + + // [GIVEN] An outgoing invoice E-Document and a payment application + CreatePaymentOccurrenceScenario(EDocument, DetailedCustLedgEntry); + EDocImplState.SetThrowPaymentOccurrenceSchedulingError(); + BindSubscription(EDocImplState); + + // [WHEN] The payment application is captured while scheduling fails + EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); + UnbindSubscription(EDocImplState); + + // [THEN] The occurrence remains available for retry and contains the scheduling error + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.FindFirst(); + Assert.AreEqual(EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status, 'A scheduling failure must leave the occurrence in Error.'); + Assert.AreEqual(1, EDocPaymentOccurrence."Retry Count", 'A scheduling failure must increment the retry count.'); + Assert.IsTrue(EDocPaymentOccurrence."Last Error".Contains('TEST PAYMENT OCCURRENCE SCHEDULING'), 'The scheduling error must be retained.'); + end; + + [Test] + procedure PaymentOccurrenceDispatcherRetriesFailedProcessing() + var + Customer: Record Customer; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; + EDocument: Record "E-Document"; + EDocPaymentOccurrenceDispatcher: Codeunit "E-Doc. Payment Occ. Dispatcher"; + EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; + begin + // [FEATURE] [AI test] + // [SCENARIO] A failed payment occurrence is retained and processed by the dispatcher retry + Initialize(Customer); + + // [GIVEN] A persisted payment occurrence whose localization processing fails + CreatePaymentOccurrenceScenario(EDocument, DetailedCustLedgEntry); + EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); + EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPaymentOccurrence.FindFirst(); + EDocImplState.SetThrowPaymentOccurrenceProcessingError(); + BindSubscription(EDocImplState); + EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + UnbindSubscription(EDocImplState); + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + Assert.AreEqual(EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status, 'Failed processing must leave the occurrence in Error.'); + Assert.AreEqual(1, EDocPaymentOccurrence."Retry Count", 'Failed processing must increment the retry count.'); + EDocPaymentOccurrence."Next Attempt At" := 0DT; + EDocPaymentOccurrence.Modify(); + + // [WHEN] The recurrent dispatcher retries the occurrence + EDocPaymentOccurrenceDispatcher.Run(); + + // [THEN] The occurrence is marked processed and its error is cleared + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + Assert.AreEqual(EDocPaymentOccurrence.Status::Processed, EDocPaymentOccurrence.Status, 'A successful retry must mark the occurrence Processed.'); + Assert.AreEqual('', EDocPaymentOccurrence."Last Error", 'A successful retry must clear the previous error.'); + end; + local procedure Initialize(var Customer: Record Customer) var EDocument: Record "E-Document"; @@ -368,6 +438,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" JobQueueEntry.SetFilter("Object ID to Run", '%1|%2', Codeunit::"E-Doc. Message Send Job", Codeunit::"E-Doc. Message Response Job"); JobQueueEntry.DeleteAll(); EDocMessage.DeleteAll(); + EDocPaymentOccurrence.DeleteAll(); EDocument.DeleteAll(); if not IsInitialized then begin @@ -380,6 +451,41 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock); end; + local procedure CreatePaymentOccurrenceScenario(var EDocument: Record "E-Document"; var DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") + var + InvoiceCustLedgerEntry: Record "Cust. Ledger Entry"; + PaymentCustLedgerEntry: Record "Cust. Ledger Entry"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + begin + SalesInvoiceHeader.Init(); + SalesInvoiceHeader."No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(SalesInvoiceHeader."No.")); + SalesInvoiceHeader.Insert(); + EDocument.Init(); + EDocument."Document No." := SalesInvoiceHeader."No."; + EDocument."Document Record ID" := SalesInvoiceHeader.RecordId; + EDocument.Direction := EDocument.Direction::Outgoing; + EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; + EDocument.Insert(); + InvoiceCustLedgerEntry.Init(); + InvoiceCustLedgerEntry."Entry No." := -1; + InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; + InvoiceCustLedgerEntry."Document No." := SalesInvoiceHeader."No."; + InvoiceCustLedgerEntry.Insert(); + PaymentCustLedgerEntry.Init(); + PaymentCustLedgerEntry."Entry No." := -2; + PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; + PaymentCustLedgerEntry.Insert(); + DetailedCustLedgEntry.Init(); + DetailedCustLedgEntry."Entry No." := -1; + DetailedCustLedgEntry."Cust. Ledger Entry No." := InvoiceCustLedgerEntry."Entry No."; + DetailedCustLedgEntry."Applied Cust. Ledger Entry No." := PaymentCustLedgerEntry."Entry No."; + DetailedCustLedgEntry."Entry Type" := DetailedCustLedgEntry."Entry Type"::Application; + DetailedCustLedgEntry."Initial Document Type" := DetailedCustLedgEntry."Initial Document Type"::Invoice; + DetailedCustLedgEntry.Amount := -100; + DetailedCustLedgEntry.SystemId := CreateGuid(); + DetailedCustLedgEntry.Insert(); + end; + local procedure CreateOutgoingEDocument(var EDocument: Record "E-Document") begin EDocument.Init(); From 26511feffee54e29b72ca94427af5dd92c4aee5e Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 01:41:43 +0200 Subject: [PATCH 41/49] compile err --- .../Processing/EDocumentBackgroundJobs.Codeunit.al | 3 +-- ...eunit.al => EDocPaymentOccDispatcher.Codeunit.al} | 2 +- ....Codeunit.al => EDocPaymentOccRunner.Codeunit.al} | 0 ...ceStatus.Enum.al => EDocPaymentOccStatus.Enum.al} | 0 .../Message/EDocPaymentOccurrenceMgt.Codeunit.al | 12 +++++++----- .../App/src/Setup/EDocumentSetup.Codeunit.al | 3 +-- .../App/src/Setup/EDocumentUpgrade.Codeunit.al | 6 +++--- .../Test/src/Mock/EDocImplState.Codeunit.al | 3 ++- .../src/Processing/EDocMessageMgtTests.Codeunit.al | 1 + 9 files changed, 16 insertions(+), 14 deletions(-) rename src/Apps/W1/EDocument/App/src/Processing/Message/{EDocPaymentOccurrenceDispatcher.Codeunit.al => EDocPaymentOccDispatcher.Codeunit.al} (97%) rename src/Apps/W1/EDocument/App/src/Processing/Message/{EDocPaymentOccurrenceRunner.Codeunit.al => EDocPaymentOccRunner.Codeunit.al} (100%) rename src/Apps/W1/EDocument/App/src/Processing/Message/{EDocPaymentOccurrenceStatus.Enum.al => EDocPaymentOccStatus.Enum.al} (100%) diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index b7f9503c683..6b8e3e4169a 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -38,8 +38,7 @@ codeunit 6133 "E-Document Background Jobs" SchedulePaymentOccurrence(EDocPaymentOccurrence); end; - [TryFunction] - internal procedure TryEnsurePaymentOccurrenceDispatcher() + internal procedure EnsurePaymentOccurrenceDispatcher() var JobQueueEntry: Record "Job Queue Entry"; BlankRecordId: RecordId; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccDispatcher.Codeunit.al similarity index 97% rename from src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al rename to src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccDispatcher.Codeunit.al index b82f583131d..18f4c77d138 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceDispatcher.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccDispatcher.Codeunit.al @@ -4,7 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument.Processing.Message; -codeunit 6435 "E-Doc. Payment Occ. Dispatcher" +codeunit 6248 "E-Doc. Payment Occ. Dispatcher" { Access = Internal; InherentEntitlements = X; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccRunner.Codeunit.al similarity index 100% rename from src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceRunner.Codeunit.al rename to src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccRunner.Codeunit.al diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccStatus.Enum.al similarity index 100% rename from src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceStatus.Enum.al rename to src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccStatus.Enum.al diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index 25a4845a459..c6b03d23190 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -133,7 +133,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Error; EDocPaymentOccurrence."Retry Count" := 1; EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); - EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay(); EDocPaymentOccurrence."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(EDocPaymentOccurrence."Last Error")); EDocPaymentOccurrence.Modify(); ClearLastError(); @@ -154,7 +154,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Processing; EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); - EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay(); EDocPaymentOccurrence.Modify(); Commit(); if Codeunit.Run(Codeunit::"E-Doc. Payment Occ. Runner", EDocPaymentOccurrence) then begin @@ -172,7 +172,7 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Error; EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); EDocPaymentOccurrence."Retry Count" += 1; - EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay; + EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay(); EDocPaymentOccurrence."Last Error" := CopyStr(LastErrorText, 1, MaxStrLen(EDocPaymentOccurrence."Last Error")); EDocPaymentOccurrence.Modify(); ClearLastError(); @@ -216,6 +216,8 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." begin end; - var - RetryDelay: Duration = 300000; + local procedure RetryDelay(): Duration + begin + exit(300000); + end; } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al index 3b24d41660c..6de33abeefb 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al @@ -18,8 +18,7 @@ codeunit 6145 "E-Document Setup" begin InstallWorkFlowTableRelation(); AddEDocumentLogToAllowedTables(); - if not EDocumentBackgroundJobs.TryEnsurePaymentOccurrenceDispatcher() then - ClearLastError(); + EDocumentBackgroundJobs.EnsurePaymentOccurrenceDispatcher(); end; local procedure AddEDocumentLogToAllowedTables() diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al index c69eca4dd7b..142607d8ff5 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al @@ -28,8 +28,7 @@ codeunit 6168 "E-Document Upgrade" #endif UpgradeDataExchV2Defs(); UpgradeEnableVATOptionsForPurchEDoc(); - if not EDocumentBackgroundJobs.TryEnsurePaymentOccurrenceDispatcher() then - ClearLastError(); + EDocumentBackgroundJobs.EnsurePaymentOccurrenceDispatcher(); end; local procedure UpgradeLogURLMaxLength() @@ -115,11 +114,12 @@ codeunit 6168 "E-Document Upgrade" UpgradeTag.SetUpgradeTag(GetEnableVATOptionsForPurchEDocTag()); end; + internal procedure GetUpgradeDataExchV2DefsTag(): Code[250] begin exit('MS-EDoc-DataExchV2Defs-20260521'); end; - + internal procedure GetEnableVATOptionsForPurchEDocTag(): Code[250] begin exit('MS-EDoc-EnableVATOptionsForPurchEDoc-20260520'); diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al index 37832ae1fcd..a96bd2c9275 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Test; using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Company; using Microsoft.Purchases.Document; @@ -21,7 +22,7 @@ codeunit 139630 "E-Doc. Impl. State" PurchDocTestBuffer: Codeunit "E-Doc. Test Buffer"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; EnableOnCheck, DisableOnCreateOutput, DisableOnCreateBatch, IsAsync2, EnableHttpData, ThrowIntegrationRuntimeError, ThrowIntegrationLoggedError : Boolean; - ThrowPaymentOccurrenceProcessingError, ThrowPaymentOccurrenceSchedulingError: Boolean; + ThrowPaymentOccurrenceProcessingError, ThrowPaymentOccurrenceSchedulingError : Boolean; EnableSourceDocumentHeaderCapture: Boolean; ThrowRuntimeError, ThrowLoggedError, ThrowBasicInfoError, ThrowCompleteInfoError, OnGetResponseSuccess, ActionHasUpdate : Boolean; #if not CLEAN26 diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index 38f512a4ca3..e5a46a1ce8d 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -429,6 +429,7 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" var EDocument: Record "E-Document"; EDocMessage: Record "E-Document Message"; + EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; JobQueueEntry: Record "Job Queue Entry"; begin if UnbindSubscription(EDocImplState) then; From 5342a13903f5bd5ae2dd987560fadf6a4c504b36 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 02:26:18 +0200 Subject: [PATCH 42/49] test --- .../src/FREInvoiceMessageTests.Codeunit.al | 24 +++++---- .../EDocumentBackgroundJobs.Codeunit.al | 5 +- .../EDocPaymentOccurrenceMgt.Codeunit.al | 11 ---- .../Test/src/Mock/EDocImplState.Codeunit.al | 14 +---- .../EDocMessageMgtTests.Codeunit.al | 53 ++++++++++++++----- 5 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al index 72cc1d696ec..4c9e2d4196e 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al @@ -12,10 +12,12 @@ using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; +using Microsoft.Foundation.Enums; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Receivables; +using System.Threading; using System.Utilities; codeunit 148151 "FR E-Invoice Message Tests" @@ -158,7 +160,6 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; FREInvoiceMessage: Record "FR E-Invoice Message"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; - EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin // [FEATURE] [AI test] // [SCENARIO] Invalid optional lifecycle configuration does not block a payment and its occurrence can be retried @@ -178,21 +179,16 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); Assert.RecordCount(EDocPaymentOccurrence, 1); EDocPaymentOccurrence.FindFirst(); - Assert.AreEqual(EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status, 'The failed lifecycle message must remain available for retry.'); FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No"); FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected); Assert.RecordCount(FREInvoiceMessage, 0); // [WHEN] The configuration is repaired and the persisted occurrence is retried EnsureCompanyInformation(); - EDocPaymentOccurrence."Next Attempt At" := 0DT; - EDocPaymentOccurrence.Modify(); - EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + RunPaymentOccurrence(EDocPaymentOccurrence); // [THEN] The French lifecycle message is created from the original occurrence Assert.RecordCount(FREInvoiceMessage, 1); - EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); - Assert.AreEqual(EDocPaymentOccurrence.Status::Processed, EDocPaymentOccurrence.Status, 'The successful retry must mark the occurrence Processed.'); end; [Test] @@ -1987,13 +1983,12 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); - Commit(); EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied); EDocPaymentOccurrence.SetRange("Source Occurrence ID", DetailedCustLedgEntry.SystemId); EDocPaymentOccurrence.FindFirst(); - EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + RunPaymentOccurrence(EDocPaymentOccurrence); end; local procedure ProcessPaymentUnapplication(EDocument: Record "E-Document"; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; NewDetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry") @@ -2002,13 +1997,20 @@ codeunit 148151 "FR E-Invoice Message Tests" EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin EDocPaymentOccurrenceMgt.ProcessUnapplication(DetailedCustLedgEntry, NewDetailedCustLedgEntry); - Commit(); EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Reversed); EDocPaymentOccurrence.SetRange("Source Occurrence ID", NewDetailedCustLedgEntry.SystemId); EDocPaymentOccurrence.FindFirst(); - EDocPaymentOccurrenceMgt.ProcessPaymentOccurrence(EDocPaymentOccurrence); + RunPaymentOccurrence(EDocPaymentOccurrence); + end; + + local procedure RunPaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + var + JobQueueEntry: Record "Job Queue Entry"; + begin + JobQueueEntry."Record ID to Process" := EDocPaymentOccurrence.RecordId; + Codeunit.Run(Codeunit::"E-Doc. Payment Occurrence Mgt.", JobQueueEntry); end; local procedure GetNextDetailedLedgerEntryNo(): Integer diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al index 6b8e3e4169a..b7ac44ef0d8 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentBackgroundJobs.Codeunit.al @@ -32,10 +32,9 @@ codeunit 6133 "E-Document Background Jobs" ScheduleEDocumentJob(Codeunit::"E-Doc. Payment Occurrence Mgt.", EDocPaymentOccurrence.RecordId(), 0); end; - [TryFunction] - procedure TrySchedulePaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") + procedure TrySchedulePaymentOccurrence(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"): Boolean begin - SchedulePaymentOccurrence(EDocPaymentOccurrence); + exit(TryScheduleEDocumentJob(Codeunit::"E-Doc. Payment Occurrence Mgt.", EDocPaymentOccurrence.RecordId(), 0)); end; internal procedure EnsurePaymentOccurrenceDispatcher() diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al index c6b03d23190..6507334ae40 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al @@ -108,7 +108,6 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." local procedure CreateOccurrence(EDocumentEntryNo: Integer; OccurrenceType: Enum "E-Doc. Payment Occurrence Type"; SourceOccurrenceID: Guid; Amount: Decimal; CurrencyCode: Code[10]; EventDate: Date; DetailedLedgerEntryNo: Integer; OriginalOccurrenceEntryNo: Integer) var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin EDocPaymentOccurrence.LockTable(); EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocumentEntryNo); @@ -129,15 +128,6 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." EDocPaymentOccurrence."Created At" := CurrentDateTime(); EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Pending; EDocPaymentOccurrence.Insert(); - if not EDocumentBackgroundJobs.TrySchedulePaymentOccurrence(EDocPaymentOccurrence) then begin - EDocPaymentOccurrence.Status := EDocPaymentOccurrence.Status::Error; - EDocPaymentOccurrence."Retry Count" := 1; - EDocPaymentOccurrence."Last Attempt At" := CurrentDateTime(); - EDocPaymentOccurrence."Next Attempt At" := CurrentDateTime() + RetryDelay(); - EDocPaymentOccurrence."Last Error" := CopyStr(GetLastErrorText(), 1, MaxStrLen(EDocPaymentOccurrence."Last Error")); - EDocPaymentOccurrence.Modify(); - ClearLastError(); - end; end; internal procedure ProcessPaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") @@ -210,7 +200,6 @@ codeunit 6536 "E-Doc. Payment Occurrence Mgt." /// Notifies localization and format apps after a payment occurrence has been persisted. /// /// The persisted payment occurrence. - [CommitBehavior(CommitBehavior::Ignore)] [IntegrationEvent(false, false)] procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin diff --git a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al index a96bd2c9275..aed745cb6eb 100644 --- a/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Mock/EDocImplState.Codeunit.al @@ -22,7 +22,7 @@ codeunit 139630 "E-Doc. Impl. State" PurchDocTestBuffer: Codeunit "E-Doc. Test Buffer"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; EnableOnCheck, DisableOnCreateOutput, DisableOnCreateBatch, IsAsync2, EnableHttpData, ThrowIntegrationRuntimeError, ThrowIntegrationLoggedError : Boolean; - ThrowPaymentOccurrenceProcessingError, ThrowPaymentOccurrenceSchedulingError : Boolean; + ThrowPaymentOccurrenceProcessingError: Boolean; EnableSourceDocumentHeaderCapture: Boolean; ThrowRuntimeError, ThrowLoggedError, ThrowBasicInfoError, ThrowCompleteInfoError, OnGetResponseSuccess, ActionHasUpdate : Boolean; #if not CLEAN26 @@ -37,13 +37,6 @@ codeunit 139630 "E-Doc. Impl. State" LibraryVariableStorage.Enqueue(EDocument); end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Document Background Jobs", 'OnBeforeScheduleEDocumentJob', '', false, false)] - local procedure OnBeforeScheduleEDocumentJob(CodeunitId: Integer; JobRecordId: RecordId) - begin - if ThrowPaymentOccurrenceSchedulingError and (CodeunitId = Codeunit::"E-Doc. Payment Occurrence Mgt.") then - Error('TEST PAYMENT OCCURRENCE SCHEDULING'); - end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. Payment Occurrence Mgt.", 'OnAfterCreatePaymentOccurrence', '', false, false)] local procedure OnAfterCreatePaymentOccurrence(var EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") begin @@ -511,11 +504,6 @@ codeunit 139630 "E-Doc. Impl. State" ThrowPaymentOccurrenceProcessingError := true; end; - internal procedure SetThrowPaymentOccurrenceSchedulingError() - begin - ThrowPaymentOccurrenceSchedulingError := true; - end; - internal procedure SetHttpResponse(HttpResponse: HttpResponseMessage) begin LocalHttpResponse := HttpResponse; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al index e5a46a1ce8d..b58c9cc6d4d 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al @@ -358,33 +358,38 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" end; [Test] - procedure PaymentOccurrenceSchedulingFailureDoesNotEscapeCapture() + procedure PaymentOccurrenceDispatcherProcessesPendingCapture() var Customer: Record Customer; DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence"; EDocument: Record "E-Document"; + EDocPaymentOccurrenceDispatcher: Codeunit "E-Doc. Payment Occ. Dispatcher"; EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt."; begin // [FEATURE] [AI test] - // [SCENARIO] A background-job scheduling failure does not fail payment occurrence capture + // [SCENARIO] The recurrent dispatcher processes a pending payment occurrence Initialize(Customer); // [GIVEN] An outgoing invoice E-Document and a payment application CreatePaymentOccurrenceScenario(EDocument, DetailedCustLedgEntry); - EDocImplState.SetThrowPaymentOccurrenceSchedulingError(); - BindSubscription(EDocImplState); - // [WHEN] The payment application is captured while scheduling fails + // [WHEN] The payment application is captured EDocPaymentOccurrenceMgt.ProcessApplication(DetailedCustLedgEntry); - UnbindSubscription(EDocImplState); - // [THEN] The occurrence remains available for retry and contains the scheduling error + // [THEN] The occurrence remains pending for the dispatcher without retry metadata EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No"); EDocPaymentOccurrence.FindFirst(); - Assert.AreEqual(EDocPaymentOccurrence.Status::Error, EDocPaymentOccurrence.Status, 'A scheduling failure must leave the occurrence in Error.'); - Assert.AreEqual(1, EDocPaymentOccurrence."Retry Count", 'A scheduling failure must increment the retry count.'); - Assert.IsTrue(EDocPaymentOccurrence."Last Error".Contains('TEST PAYMENT OCCURRENCE SCHEDULING'), 'The scheduling error must be retained.'); + Assert.AreEqual(EDocPaymentOccurrence.Status::Pending, EDocPaymentOccurrence.Status, 'A captured occurrence must remain pending for the dispatcher.'); + Assert.AreEqual(0, EDocPaymentOccurrence."Retry Count", 'A pending occurrence must not have a retry count.'); + Assert.AreEqual('', EDocPaymentOccurrence."Last Error", 'A pending occurrence must not contain an error.'); + + // [WHEN] The recurrent dispatcher runs + EDocPaymentOccurrenceDispatcher.Run(); + + // [THEN] The occurrence is processed + EDocPaymentOccurrence.Get(EDocPaymentOccurrence."Entry No."); + Assert.AreEqual(EDocPaymentOccurrence.Status::Processed, EDocPaymentOccurrence.Status, 'The dispatcher must process a pending occurrence.'); end; [Test] @@ -468,16 +473,16 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" EDocument."Document Type" := EDocument."Document Type"::"Sales Invoice"; EDocument.Insert(); InvoiceCustLedgerEntry.Init(); - InvoiceCustLedgerEntry."Entry No." := -1; + InvoiceCustLedgerEntry."Entry No." := GetUnusedCustLedgerEntryNo(); InvoiceCustLedgerEntry."Document Type" := InvoiceCustLedgerEntry."Document Type"::Invoice; InvoiceCustLedgerEntry."Document No." := SalesInvoiceHeader."No."; InvoiceCustLedgerEntry.Insert(); PaymentCustLedgerEntry.Init(); - PaymentCustLedgerEntry."Entry No." := -2; + PaymentCustLedgerEntry."Entry No." := GetUnusedCustLedgerEntryNo(); PaymentCustLedgerEntry."Document Type" := PaymentCustLedgerEntry."Document Type"::Payment; PaymentCustLedgerEntry.Insert(); DetailedCustLedgEntry.Init(); - DetailedCustLedgEntry."Entry No." := -1; + DetailedCustLedgEntry."Entry No." := GetUnusedDetailedCustLedgerEntryNo(); DetailedCustLedgEntry."Cust. Ledger Entry No." := InvoiceCustLedgerEntry."Entry No."; DetailedCustLedgEntry."Applied Cust. Ledger Entry No." := PaymentCustLedgerEntry."Entry No."; DetailedCustLedgEntry."Entry Type" := DetailedCustLedgEntry."Entry Type"::Application; @@ -487,6 +492,28 @@ codeunit 139898 "E-Doc. Message Mgt. Tests" DetailedCustLedgEntry.Insert(); end; + local procedure GetUnusedCustLedgerEntryNo(): Integer + var + CustLedgerEntry: Record "Cust. Ledger Entry"; + EntryNo: Integer; + begin + EntryNo := -1; + while CustLedgerEntry.Get(EntryNo) do + EntryNo -= 1; + exit(EntryNo); + end; + + local procedure GetUnusedDetailedCustLedgerEntryNo(): Integer + var + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + EntryNo: Integer; + begin + EntryNo := -1; + while DetailedCustLedgEntry.Get(EntryNo) do + EntryNo -= 1; + exit(EntryNo); + end; + local procedure CreateOutgoingEDocument(var EDocument: Record "E-Document") begin EDocument.Init(); From 11f4ddd5f893bfc4d173885368ba50ed9160c04a Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 09:33:21 +0200 Subject: [PATCH 43/49] DataClassificationEvalData --- .../app/src/Core/FREInvoiceMessageMgt.Codeunit.al | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al index eed949d754b..5da66ea031b 100644 --- a/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al +++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al @@ -12,6 +12,7 @@ using Microsoft.Finance.VAT.Ledger; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Company; using Microsoft.Sales.Receivables; +using Microsoft.Utilities; using System.Utilities; codeunit 10975 "FR E-Invoice Message Mgt." @@ -59,6 +60,15 @@ codeunit 10975 "FR E-Invoice Message Mgt." CreatePaymentLifecycleMessage(EDocPaymentOccurrence); end; + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Data Classification Eval. Data", 'OnCreateEvaluationDataOnAfterClassifyTablesToNormal', '', false, false)] + local procedure ClassifyDataSensitivity() + var + DataClassificationEvalData: Codeunit "Data Classification Eval. Data"; + begin + DataClassificationEvalData.SetTableFieldsToNormal(Database::"FR E-Invoice Message"); + DataClassificationEvalData.SetTableFieldsToNormal(Database::"FR E-Invoice Message VAT"); + end; + local procedure CreatePaymentLifecycleMessage(EDocPaymentOccurrence: Record "E-Doc. Payment Occurrence") var CollectedMessage: Record "FR E-Invoice Message"; From e279968ac375d886a5bc548359573de2f80aa177 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Tue, 1 Sep 2026 13:04:21 +0200 Subject: [PATCH 44/49] removed code from install and upgrade cu --- src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al | 3 --- .../W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al index 6de33abeefb..d64b2b4f844 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentSetup.Codeunit.al @@ -13,12 +13,9 @@ codeunit 6145 "E-Document Setup" Subtype = Install; trigger OnInstallAppPerCompany() - var - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin InstallWorkFlowTableRelation(); AddEDocumentLogToAllowedTables(); - EDocumentBackgroundJobs.EnsurePaymentOccurrenceDispatcher(); end; local procedure AddEDocumentLogToAllowedTables() diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al index 142607d8ff5..f8b761255ab 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al @@ -19,8 +19,6 @@ codeunit 6168 "E-Document Upgrade" InherentEntitlements = X; trigger OnUpgradePerCompany() - var - EDocumentBackgroundJobs: Codeunit "E-Document Background Jobs"; begin UpgradeLogURLMaxLength(); #if not CLEAN29 @@ -28,7 +26,6 @@ codeunit 6168 "E-Document Upgrade" #endif UpgradeDataExchV2Defs(); UpgradeEnableVATOptionsForPurchEDoc(); - EDocumentBackgroundJobs.EnsurePaymentOccurrenceDispatcher(); end; local procedure UpgradeLogURLMaxLength() From 89eb7f8db8ce1cd3772178836f5f307761cf3449 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 12 Sep 2026 10:57:33 +0200 Subject: [PATCH 45/49] object ID fix --- .../App/src/Processing/EDocItemChargeMapping.Codeunit.al | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al index 87da816483a..e1b7eec02e3 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocItemChargeMapping.Codeunit.al @@ -9,7 +9,7 @@ using Microsoft.Inventory.Item; using Microsoft.Inventory.Ledger; using Microsoft.Sales.History; -codeunit 6532 "E-Doc. Item Charge Mapping" +codeunit 6246 "E-Doc. Item Charge Mapping" { Permissions = tabledata "Item Charge" = r, From 71299e49819a3aeee76c8cc2c69050647a20e63c Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 12 Sep 2026 10:32:50 +0200 Subject: [PATCH 46/49] Export requested receipt dates in PEPPOL orders --- .../ExportPurchaseOrderPEPPOL30.Codeunit.al | 28 +++++++ .../src/PEPPOLBISBillingTests.Codeunit.al | 81 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al b/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al index 541bd4be2b7..d4cc2cbc339 100644 --- a/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al +++ b/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al @@ -260,6 +260,7 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.AddNonEmptyNode(AddressNode, 'CountrySubentity', CountrySubentity, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddElement(AddressNode, 'Country', '', CacNamespaceTok, CountryNode); this.XMLDOMManagement.AddElement(CountryNode, 'IdentificationCode', IdentificationCode, CbcNamespaceTok, ChildNode); + this.AddRequestedDeliveryPeriod(DeliveryNode, PurchaseHeader."Requested Receipt Date"); end; local procedure AddPaymentTerms(PurchaseHeader: Record "Purchase Header") @@ -368,6 +369,7 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.XMLDOMManagement.AddAttribute(ChildNode, 'unitCode', UnitCode); this.XMLDOMManagement.AddElement(LineItemNode, 'LineExtensionAmount', InvoiceLineExtensionAmount, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddAttribute(ChildNode, 'currencyID', InvLinePriceAmountCurrencyID); + this.AddRequestedDelivery(PurchaseLine, LineItemNode); this.XMLDOMManagement.AddElement(LineItemNode, 'Price', '', CacNamespaceTok, PriceNode); this.XMLDOMManagement.AddElement(PriceNode, 'PriceAmount', InvoiceLinePriceAmount, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddAttribute(ChildNode, 'currencyID', InvLinePriceAmountCurrencyID); @@ -392,6 +394,32 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.XMLDOMManagement.AddElement(TaxSchemeNode, 'ID', ClassifiedTaxCategorySchemeID, CbcNamespaceTok, ChildNode); end; + local procedure AddRequestedDelivery(PurchaseLine: Record "Purchase Line"; LineItemNode: XmlNode) + var + DeliveryNode: XmlNode; + begin + if PurchaseLine."Requested Receipt Date" = 0D then + exit; + + this.XMLDOMManagement.AddElement(LineItemNode, 'Delivery', '', CacNamespaceTok, DeliveryNode); + this.AddRequestedDeliveryPeriod(DeliveryNode, PurchaseLine."Requested Receipt Date"); + end; + + local procedure AddRequestedDeliveryPeriod(DeliveryNode: XmlNode; RequestedReceiptDate: Date) + var + RequestedDeliveryPeriodNode: XmlNode; + ChildNode: XmlNode; + RequestedReceiptDateText: Text; + begin + if RequestedReceiptDate = 0D then + exit; + + RequestedReceiptDateText := Format(RequestedReceiptDate, 0, 9); + this.XMLDOMManagement.AddElement(DeliveryNode, 'RequestedDeliveryPeriod', '', CacNamespaceTok, RequestedDeliveryPeriodNode); + this.XMLDOMManagement.AddElement(RequestedDeliveryPeriodNode, 'StartDate', RequestedReceiptDateText, CbcNamespaceTok, ChildNode); + this.XMLDOMManagement.AddElement(RequestedDeliveryPeriodNode, 'EndDate', RequestedReceiptDateText, CbcNamespaceTok, ChildNode); + end; + local procedure AddPartyTaxScheme(PartyNode: XmlNode) var PEPPOLPartyInfo: Interface "PEPPOL Purchase Party Info Provider"; diff --git a/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al b/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al index e14c1870a02..c996598acdb 100644 --- a/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al +++ b/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al @@ -16,6 +16,8 @@ using Microsoft.Foundation.Enums; using Microsoft.Foundation.Reporting; using Microsoft.Inventory.Item; using Microsoft.Peppol; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.Setup; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; @@ -43,6 +45,7 @@ codeunit 139236 "PEPPOL BIS BillingTests" var LibraryService: Codeunit "Library - Service"; LibraryInventory: Codeunit "Library - Inventory"; + LibraryPurchase: Codeunit "Library - Purchase"; LibrarySales: Codeunit "Library - Sales"; LibraryERM: Codeunit "Library - ERM"; LibraryRandom: Codeunit "Library - Random"; @@ -56,6 +59,67 @@ codeunit 139236 "PEPPOL BIS BillingTests" WrongFileNameErr: Label 'File name should be: %1', Comment = '%1 - Client File Name'; InvoiceNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true; CreditNoteNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', Locked = true; + OrderNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Order-2', Locked = true; + + [Test] + procedure ExportPurchaseOrderRequestedReceiptDates() + var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + TempBlob: Codeunit "Temp Blob"; + HeaderRequestedReceiptDate: Date; + LineRequestedReceiptDate: Date; + begin + // [FEATURE] [Order] [Delivery Date] + // [SCENARIO] Requested receipt dates are exported as PEPPOL requested delivery periods + Initialize(); + + // [GIVEN] A purchase order with different requested receipt dates on the header and line + HeaderRequestedReceiptDate := WorkDate() + 1; + LineRequestedReceiptDate := WorkDate() + 2; + CreatePurchaseOrder(PurchaseHeader, PurchaseLine); + PurchaseHeader.Validate("Requested Receipt Date", HeaderRequestedReceiptDate); + PurchaseHeader.Modify(true); + PurchaseLine.Validate("Requested Receipt Date", LineRequestedReceiptDate); + PurchaseLine.Modify(true); + + // [WHEN] The purchase order is exported to PEPPOL XML + ExportPurchaseOrderToBlob(PurchaseHeader, TempBlob); + + // [THEN] Each date is exported as both the start and end of its requested delivery period + InitXPathXMLReader(TempBlob, OrderNamespaceTxt); + LibraryXPathXMLReader.VerifyNodeValueByXPath('/*/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:StartDate', Format(HeaderRequestedReceiptDate, 0, 9)); + LibraryXPathXMLReader.VerifyNodeValueByXPath('/*/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:EndDate', Format(HeaderRequestedReceiptDate, 0, 9)); + LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:StartDate', Format(LineRequestedReceiptDate, 0, 9)); + LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:EndDate', Format(LineRequestedReceiptDate, 0, 9)); + end; + + [Test] + procedure ExportPurchaseOrderWithoutRequestedReceiptDates() + var + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + TempBlob: Codeunit "Temp Blob"; + begin + // [FEATURE] [Order] [Delivery Date] + // [SCENARIO] Blank requested receipt dates do not create requested delivery periods + Initialize(); + + // [GIVEN] A purchase order without requested receipt dates + CreatePurchaseOrder(PurchaseHeader, PurchaseLine); + PurchaseHeader.Validate("Requested Receipt Date", 0D); + PurchaseHeader.Modify(true); + PurchaseLine.Validate("Requested Receipt Date", 0D); + PurchaseLine.Modify(true); + + // [WHEN] The purchase order is exported to PEPPOL XML + ExportPurchaseOrderToBlob(PurchaseHeader, TempBlob); + + // [THEN] No requested delivery period is exported at header or line level + InitXPathXMLReader(TempBlob, OrderNamespaceTxt); + LibraryXPathXMLReader.VerifyNodeAbsence('/*/cac:Delivery/cac:RequestedDeliveryPeriod'); + LibraryXPathXMLReader.VerifyNodeAbsence('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod'); + end; [Test] [Scope('OnPrem')] @@ -1632,12 +1696,14 @@ codeunit 139236 "PEPPOL BIS BillingTests" LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralLedgerSetup(); LibraryERMCountryData.UpdateGeneralPostingSetup(); + LibraryERMCountryData.UpdatePurchasesPayablesSetup(); LibraryERMCountryData.UpdateSalesReceivablesSetup(); LibraryERMCountryData.UpdateLocalData(); UpdateElectronicDocumentFormatSetup(); LibraryService.SetupServiceMgtNoSeries(); LibrarySetupStorage.Save(DATABASE::"Company Information"); LibrarySetupStorage.Save(DATABASE::"General Ledger Setup"); + LibrarySetupStorage.Save(DATABASE::"Purchases & Payables Setup"); IsInitialized := true; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"PEPPOL BIS BillingTests"); @@ -1646,6 +1712,21 @@ codeunit 139236 "PEPPOL BIS BillingTests" ConfigureVATPostingSetup(); end; + local procedure CreatePurchaseOrder(var PurchaseHeader: Record "Purchase Header"; var PurchaseLine: Record "Purchase Line") + begin + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, LibraryPurchase.CreateVendorNo()); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, LibraryInventory.CreateItemNo(), 1); + end; + + local procedure ExportPurchaseOrderToBlob(PurchaseHeader: Record "Purchase Header"; var TempBlob: Codeunit "Temp Blob") + var + ExportPurchaseOrderPEPPOL30: Codeunit "Export Purchase Order PEPPOL30"; + begin + ExportPurchaseOrderPEPPOL30.SetFormat(Enum::"PEPPOL 3.0 Purchase"::"PEPPOL 3.0 - Purchase"); + ExportPurchaseOrderPEPPOL30.Run(PurchaseHeader); + ExportPurchaseOrderPEPPOL30.GetPurchaseOrderXML(TempBlob); + end; + local procedure AddCustPEPPOLIdentifier(CustNo: Code[20]) var Cust: Record Customer; From 357036c2e22d61f772e0ee7cfa2c9611a884ea11 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Sat, 12 Sep 2026 10:43:21 +0200 Subject: [PATCH 47/49] Apply PEPPOL requested delivery dates to sales lines --- .../Sales/EDocSalesDocHelper.Codeunit.al | 2 + .../Processing/EDocProcessTest.Codeunit.al | 67 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al index c0b0c97455a..306fe68b5c9 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al @@ -40,6 +40,8 @@ codeunit 6427 "E-Doc. Sales Doc. Helper" SalesLine."Variant Code" := EDocSalesLine."[BC] Variant Code"; SalesLine.Type := EDocSalesLine."[BC] Sales Line Type"; SalesLine.Validate("No.", EDocSalesLine."[BC] Sales Line No."); + if EDocSalesLine."Requested Delivery Date" <> 0D then + SalesLine.Validate("Requested Delivery Date", EDocSalesLine."Requested Delivery Date"); if (SalesLine.Type = SalesLine.Type::"G/L Account") and HasTotalDiscount then SalesLine.Validate("Allow Invoice Disc.", true); SalesLine.Description := EDocSalesLine.Description; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al index 6a8658b307f..f4ef7a2e616 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al @@ -1362,6 +1362,65 @@ codeunit 139883 "E-Doc Process Test" Assert.AreEqual("Sales Document Type"::Order, SalesHeader."Document Type", 'The Sales Header Document Type should be Order.'); end; + [Test] + procedure FinishDraftSalesOrder_AppliesRequestedDeliveryDates() + var + EDocument: Record "E-Document"; + EDocSalesHeader: Record "E-Document Sales Header"; + EDocSalesLine: Record "E-Document Sales Line"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + Item: Record Item; + EDocImport: Codeunit "E-Doc. Import"; + EDocumentProcessing: Codeunit "E-Document Processing"; + HeaderRequestedDeliveryDate: Date; + LineRequestedDeliveryDate: Date; + begin + // [SCENARIO] Requested delivery dates from a PEPPOL Order are applied to the Sales Order before BC calculates line dates. + Initialize(Enum::"Service Integration"::"Mock"); + HeaderRequestedDeliveryDate := DMY2Date(15, 2, 2026); + LineRequestedDeliveryDate := DMY2Date(20, 2, 2026); + + // [GIVEN] A PEPPOL Order with a header requested delivery date and a different date on its first line + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; + LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-order-standard.xml', TempEDocImportParameters); + EDocument.Get(EDocument."Entry No"); + + LibraryEDoc.GetGenericItem(Item); + EDocSalesHeader.GetFromEDocument(EDocument); + EDocSalesHeader."[BC] Customer No." := Customer."No."; + EDocSalesHeader.Modify(); + EDocSalesLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + if EDocSalesLine.FindSet() then + repeat + EDocSalesLine."[BC] Sales Line Type" := "Sales Line Type"::Item; + EDocSalesLine."[BC] Sales Line No." := Item."No."; + EDocSalesLine.Modify(); + until EDocSalesLine.Next() = 0; + + EDocument."Document Type" := "E-Document Type"::"Sales Order"; + EDocument.Modify(); + EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Draft Ready"); + + // [WHEN] The draft is applied to a Sales Order + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); + EDocument.Get(EDocument."Entry No"); + + // [THEN] The header date is applied before lines and the first line date overrides it + SalesHeader.Get(EDocument."Document Record ID"); + Assert.AreEqual(HeaderRequestedDeliveryDate, SalesHeader."Requested Delivery Date", 'The header Requested Delivery Date should match the PEPPOL Order.'); + SalesLine.SetRange("Document Type", SalesHeader."Document Type"); + SalesLine.SetRange("Document No.", SalesHeader."No."); + SalesLine.FindSet(); + VerifySalesLineDates(SalesLine, LineRequestedDeliveryDate); + + // [THEN] A line without its own date inherits the header date + SalesLine.Next(); + VerifySalesLineDates(SalesLine, HeaderRequestedDeliveryDate); + end; + [Test] procedure FinishDraftSalesOrder_CanBeUndone() var @@ -1500,6 +1559,14 @@ codeunit 139883 "E-Doc Process Test" ExistingSalesHeader.Delete(); end; + local procedure VerifySalesLineDates(SalesLine: Record "Sales Line"; ExpectedRequestedDeliveryDate: Date) + begin + Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Requested Delivery Date", 'The line Requested Delivery Date is incorrect.'); + Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Planned Delivery Date", 'The line Planned Delivery Date is incorrect.'); + Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Planned Shipment Date", 'The line Planned Shipment Date is incorrect.'); + Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Shipment Date", 'The line Shipment Date is incorrect.'); + end; + #endregion local procedure Initialize(Integration: Enum "Service Integration") From 405754d9046eebf044ca5ccfa765d9348273d4ad Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 14 Sep 2026 09:44:22 +0200 Subject: [PATCH 48/49] Revert "Apply PEPPOL requested delivery dates to sales lines" This reverts commit 357036c2e22d61f772e0ee7cfa2c9611a884ea11. --- .../Sales/EDocSalesDocHelper.Codeunit.al | 2 - .../Processing/EDocProcessTest.Codeunit.al | 67 ------------------- 2 files changed, 69 deletions(-) diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al index 306fe68b5c9..c0b0c97455a 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Sales/EDocSalesDocHelper.Codeunit.al @@ -40,8 +40,6 @@ codeunit 6427 "E-Doc. Sales Doc. Helper" SalesLine."Variant Code" := EDocSalesLine."[BC] Variant Code"; SalesLine.Type := EDocSalesLine."[BC] Sales Line Type"; SalesLine.Validate("No.", EDocSalesLine."[BC] Sales Line No."); - if EDocSalesLine."Requested Delivery Date" <> 0D then - SalesLine.Validate("Requested Delivery Date", EDocSalesLine."Requested Delivery Date"); if (SalesLine.Type = SalesLine.Type::"G/L Account") and HasTotalDiscount then SalesLine.Validate("Allow Invoice Disc.", true); SalesLine.Description := EDocSalesLine.Description; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al index f4ef7a2e616..6a8658b307f 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al @@ -1362,65 +1362,6 @@ codeunit 139883 "E-Doc Process Test" Assert.AreEqual("Sales Document Type"::Order, SalesHeader."Document Type", 'The Sales Header Document Type should be Order.'); end; - [Test] - procedure FinishDraftSalesOrder_AppliesRequestedDeliveryDates() - var - EDocument: Record "E-Document"; - EDocSalesHeader: Record "E-Document Sales Header"; - EDocSalesLine: Record "E-Document Sales Line"; - TempEDocImportParameters: Record "E-Doc. Import Parameters"; - SalesHeader: Record "Sales Header"; - SalesLine: Record "Sales Line"; - Item: Record Item; - EDocImport: Codeunit "E-Doc. Import"; - EDocumentProcessing: Codeunit "E-Document Processing"; - HeaderRequestedDeliveryDate: Date; - LineRequestedDeliveryDate: Date; - begin - // [SCENARIO] Requested delivery dates from a PEPPOL Order are applied to the Sales Order before BC calculates line dates. - Initialize(Enum::"Service Integration"::"Mock"); - HeaderRequestedDeliveryDate := DMY2Date(15, 2, 2026); - LineRequestedDeliveryDate := DMY2Date(20, 2, 2026); - - // [GIVEN] A PEPPOL Order with a header requested delivery date and a different date on its first line - TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; - LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-order-standard.xml', TempEDocImportParameters); - EDocument.Get(EDocument."Entry No"); - - LibraryEDoc.GetGenericItem(Item); - EDocSalesHeader.GetFromEDocument(EDocument); - EDocSalesHeader."[BC] Customer No." := Customer."No."; - EDocSalesHeader.Modify(); - EDocSalesLine.SetRange("E-Document Entry No.", EDocument."Entry No"); - if EDocSalesLine.FindSet() then - repeat - EDocSalesLine."[BC] Sales Line Type" := "Sales Line Type"::Item; - EDocSalesLine."[BC] Sales Line No." := Item."No."; - EDocSalesLine.Modify(); - until EDocSalesLine.Next() = 0; - - EDocument."Document Type" := "E-Document Type"::"Sales Order"; - EDocument.Modify(); - EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Draft Ready"); - - // [WHEN] The draft is applied to a Sales Order - TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); - EDocument.Get(EDocument."Entry No"); - - // [THEN] The header date is applied before lines and the first line date overrides it - SalesHeader.Get(EDocument."Document Record ID"); - Assert.AreEqual(HeaderRequestedDeliveryDate, SalesHeader."Requested Delivery Date", 'The header Requested Delivery Date should match the PEPPOL Order.'); - SalesLine.SetRange("Document Type", SalesHeader."Document Type"); - SalesLine.SetRange("Document No.", SalesHeader."No."); - SalesLine.FindSet(); - VerifySalesLineDates(SalesLine, LineRequestedDeliveryDate); - - // [THEN] A line without its own date inherits the header date - SalesLine.Next(); - VerifySalesLineDates(SalesLine, HeaderRequestedDeliveryDate); - end; - [Test] procedure FinishDraftSalesOrder_CanBeUndone() var @@ -1559,14 +1500,6 @@ codeunit 139883 "E-Doc Process Test" ExistingSalesHeader.Delete(); end; - local procedure VerifySalesLineDates(SalesLine: Record "Sales Line"; ExpectedRequestedDeliveryDate: Date) - begin - Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Requested Delivery Date", 'The line Requested Delivery Date is incorrect.'); - Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Planned Delivery Date", 'The line Planned Delivery Date is incorrect.'); - Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Planned Shipment Date", 'The line Planned Shipment Date is incorrect.'); - Assert.AreEqual(ExpectedRequestedDeliveryDate, SalesLine."Shipment Date", 'The line Shipment Date is incorrect.'); - end; - #endregion local procedure Initialize(Integration: Enum "Service Integration") From f3783b8b278ef1617c75f8371fe45c36dff4c9b9 Mon Sep 17 00:00:00 2001 From: djukicmilica Date: Mon, 14 Sep 2026 09:44:22 +0200 Subject: [PATCH 49/49] Revert "Export requested receipt dates in PEPPOL orders" This reverts commit 71299e49819a3aeee76c8cc2c69050647a20e63c. --- .../ExportPurchaseOrderPEPPOL30.Codeunit.al | 28 ------- .../src/PEPPOLBISBillingTests.Codeunit.al | 81 ------------------- 2 files changed, 109 deletions(-) diff --git a/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al b/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al index d4cc2cbc339..541bd4be2b7 100644 --- a/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al +++ b/src/Apps/W1/PEPPOL/App/src/Purchases/ExportPurchaseOrderPEPPOL30.Codeunit.al @@ -260,7 +260,6 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.AddNonEmptyNode(AddressNode, 'CountrySubentity', CountrySubentity, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddElement(AddressNode, 'Country', '', CacNamespaceTok, CountryNode); this.XMLDOMManagement.AddElement(CountryNode, 'IdentificationCode', IdentificationCode, CbcNamespaceTok, ChildNode); - this.AddRequestedDeliveryPeriod(DeliveryNode, PurchaseHeader."Requested Receipt Date"); end; local procedure AddPaymentTerms(PurchaseHeader: Record "Purchase Header") @@ -369,7 +368,6 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.XMLDOMManagement.AddAttribute(ChildNode, 'unitCode', UnitCode); this.XMLDOMManagement.AddElement(LineItemNode, 'LineExtensionAmount', InvoiceLineExtensionAmount, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddAttribute(ChildNode, 'currencyID', InvLinePriceAmountCurrencyID); - this.AddRequestedDelivery(PurchaseLine, LineItemNode); this.XMLDOMManagement.AddElement(LineItemNode, 'Price', '', CacNamespaceTok, PriceNode); this.XMLDOMManagement.AddElement(PriceNode, 'PriceAmount', InvoiceLinePriceAmount, CbcNamespaceTok, ChildNode); this.XMLDOMManagement.AddAttribute(ChildNode, 'currencyID', InvLinePriceAmountCurrencyID); @@ -394,32 +392,6 @@ codeunit 37202 "Export Purchase Order PEPPOL30" this.XMLDOMManagement.AddElement(TaxSchemeNode, 'ID', ClassifiedTaxCategorySchemeID, CbcNamespaceTok, ChildNode); end; - local procedure AddRequestedDelivery(PurchaseLine: Record "Purchase Line"; LineItemNode: XmlNode) - var - DeliveryNode: XmlNode; - begin - if PurchaseLine."Requested Receipt Date" = 0D then - exit; - - this.XMLDOMManagement.AddElement(LineItemNode, 'Delivery', '', CacNamespaceTok, DeliveryNode); - this.AddRequestedDeliveryPeriod(DeliveryNode, PurchaseLine."Requested Receipt Date"); - end; - - local procedure AddRequestedDeliveryPeriod(DeliveryNode: XmlNode; RequestedReceiptDate: Date) - var - RequestedDeliveryPeriodNode: XmlNode; - ChildNode: XmlNode; - RequestedReceiptDateText: Text; - begin - if RequestedReceiptDate = 0D then - exit; - - RequestedReceiptDateText := Format(RequestedReceiptDate, 0, 9); - this.XMLDOMManagement.AddElement(DeliveryNode, 'RequestedDeliveryPeriod', '', CacNamespaceTok, RequestedDeliveryPeriodNode); - this.XMLDOMManagement.AddElement(RequestedDeliveryPeriodNode, 'StartDate', RequestedReceiptDateText, CbcNamespaceTok, ChildNode); - this.XMLDOMManagement.AddElement(RequestedDeliveryPeriodNode, 'EndDate', RequestedReceiptDateText, CbcNamespaceTok, ChildNode); - end; - local procedure AddPartyTaxScheme(PartyNode: XmlNode) var PEPPOLPartyInfo: Interface "PEPPOL Purchase Party Info Provider"; diff --git a/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al b/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al index c996598acdb..e14c1870a02 100644 --- a/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al +++ b/src/Apps/W1/PEPPOL/Test/src/PEPPOLBISBillingTests.Codeunit.al @@ -16,8 +16,6 @@ using Microsoft.Foundation.Enums; using Microsoft.Foundation.Reporting; using Microsoft.Inventory.Item; using Microsoft.Peppol; -using Microsoft.Purchases.Document; -using Microsoft.Purchases.Setup; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; @@ -45,7 +43,6 @@ codeunit 139236 "PEPPOL BIS BillingTests" var LibraryService: Codeunit "Library - Service"; LibraryInventory: Codeunit "Library - Inventory"; - LibraryPurchase: Codeunit "Library - Purchase"; LibrarySales: Codeunit "Library - Sales"; LibraryERM: Codeunit "Library - ERM"; LibraryRandom: Codeunit "Library - Random"; @@ -59,67 +56,6 @@ codeunit 139236 "PEPPOL BIS BillingTests" WrongFileNameErr: Label 'File name should be: %1', Comment = '%1 - Client File Name'; InvoiceNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', Locked = true; CreditNoteNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2', Locked = true; - OrderNamespaceTxt: Label 'urn:oasis:names:specification:ubl:schema:xsd:Order-2', Locked = true; - - [Test] - procedure ExportPurchaseOrderRequestedReceiptDates() - var - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - TempBlob: Codeunit "Temp Blob"; - HeaderRequestedReceiptDate: Date; - LineRequestedReceiptDate: Date; - begin - // [FEATURE] [Order] [Delivery Date] - // [SCENARIO] Requested receipt dates are exported as PEPPOL requested delivery periods - Initialize(); - - // [GIVEN] A purchase order with different requested receipt dates on the header and line - HeaderRequestedReceiptDate := WorkDate() + 1; - LineRequestedReceiptDate := WorkDate() + 2; - CreatePurchaseOrder(PurchaseHeader, PurchaseLine); - PurchaseHeader.Validate("Requested Receipt Date", HeaderRequestedReceiptDate); - PurchaseHeader.Modify(true); - PurchaseLine.Validate("Requested Receipt Date", LineRequestedReceiptDate); - PurchaseLine.Modify(true); - - // [WHEN] The purchase order is exported to PEPPOL XML - ExportPurchaseOrderToBlob(PurchaseHeader, TempBlob); - - // [THEN] Each date is exported as both the start and end of its requested delivery period - InitXPathXMLReader(TempBlob, OrderNamespaceTxt); - LibraryXPathXMLReader.VerifyNodeValueByXPath('/*/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:StartDate', Format(HeaderRequestedReceiptDate, 0, 9)); - LibraryXPathXMLReader.VerifyNodeValueByXPath('/*/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:EndDate', Format(HeaderRequestedReceiptDate, 0, 9)); - LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:StartDate', Format(LineRequestedReceiptDate, 0, 9)); - LibraryXPathXMLReader.VerifyNodeValueByXPath('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod/cbc:EndDate', Format(LineRequestedReceiptDate, 0, 9)); - end; - - [Test] - procedure ExportPurchaseOrderWithoutRequestedReceiptDates() - var - PurchaseHeader: Record "Purchase Header"; - PurchaseLine: Record "Purchase Line"; - TempBlob: Codeunit "Temp Blob"; - begin - // [FEATURE] [Order] [Delivery Date] - // [SCENARIO] Blank requested receipt dates do not create requested delivery periods - Initialize(); - - // [GIVEN] A purchase order without requested receipt dates - CreatePurchaseOrder(PurchaseHeader, PurchaseLine); - PurchaseHeader.Validate("Requested Receipt Date", 0D); - PurchaseHeader.Modify(true); - PurchaseLine.Validate("Requested Receipt Date", 0D); - PurchaseLine.Modify(true); - - // [WHEN] The purchase order is exported to PEPPOL XML - ExportPurchaseOrderToBlob(PurchaseHeader, TempBlob); - - // [THEN] No requested delivery period is exported at header or line level - InitXPathXMLReader(TempBlob, OrderNamespaceTxt); - LibraryXPathXMLReader.VerifyNodeAbsence('/*/cac:Delivery/cac:RequestedDeliveryPeriod'); - LibraryXPathXMLReader.VerifyNodeAbsence('//cac:OrderLine/cac:LineItem/cac:Delivery/cac:RequestedDeliveryPeriod'); - end; [Test] [Scope('OnPrem')] @@ -1696,14 +1632,12 @@ codeunit 139236 "PEPPOL BIS BillingTests" LibraryERMCountryData.CreateVATData(); LibraryERMCountryData.UpdateGeneralLedgerSetup(); LibraryERMCountryData.UpdateGeneralPostingSetup(); - LibraryERMCountryData.UpdatePurchasesPayablesSetup(); LibraryERMCountryData.UpdateSalesReceivablesSetup(); LibraryERMCountryData.UpdateLocalData(); UpdateElectronicDocumentFormatSetup(); LibraryService.SetupServiceMgtNoSeries(); LibrarySetupStorage.Save(DATABASE::"Company Information"); LibrarySetupStorage.Save(DATABASE::"General Ledger Setup"); - LibrarySetupStorage.Save(DATABASE::"Purchases & Payables Setup"); IsInitialized := true; LibraryTestInitialize.OnAfterTestSuiteInitialize(Codeunit::"PEPPOL BIS BillingTests"); @@ -1712,21 +1646,6 @@ codeunit 139236 "PEPPOL BIS BillingTests" ConfigureVATPostingSetup(); end; - local procedure CreatePurchaseOrder(var PurchaseHeader: Record "Purchase Header"; var PurchaseLine: Record "Purchase Line") - begin - LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, LibraryPurchase.CreateVendorNo()); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, LibraryInventory.CreateItemNo(), 1); - end; - - local procedure ExportPurchaseOrderToBlob(PurchaseHeader: Record "Purchase Header"; var TempBlob: Codeunit "Temp Blob") - var - ExportPurchaseOrderPEPPOL30: Codeunit "Export Purchase Order PEPPOL30"; - begin - ExportPurchaseOrderPEPPOL30.SetFormat(Enum::"PEPPOL 3.0 Purchase"::"PEPPOL 3.0 - Purchase"); - ExportPurchaseOrderPEPPOL30.Run(PurchaseHeader); - ExportPurchaseOrderPEPPOL30.GetPurchaseOrderXML(TempBlob); - end; - local procedure AddCustPEPPOLIdentifier(CustNo: Code[20]) var Cust: Record Customer;