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..bb817cedc60
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/Permissions/EReportingFRUser.PermissionSet.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.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,
+ 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 Profile Validator" = 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/CIIXMLBuilder.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/CIIXMLBuilder.Codeunit.al
index fb468e4b730..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
@@ -158,6 +158,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
@@ -175,6 +176,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;
@@ -268,6 +298,31 @@ codeunit 10978 "CII XML Builder"
AgreementElement.Add(BuyerElement);
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";
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..b1538b6d033
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessage.Table.al
@@ -0,0 +1,158 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+ }
+ field(14; "External Message ID"; Text[250])
+ {
+ Caption = 'External Message ID';
+ DataClassification = CustomerContent;
+ }
+ field(15; "Received At"; DateTime)
+ {
+ 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;
+ }
+ 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
+ {
+ 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.")
+ {
+ }
+ 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..a529ae840f0
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageAPI.Codeunit.al
@@ -0,0 +1,190 @@
+// ------------------------------------------------------------------------------------------------
+// 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.");
+ ValidateLifecycleTransition(EDocument."Entry No", MessageType);
+
+ 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 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;
+ 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);
+ '213', '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';
+ 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
new file mode 100644
index 00000000000..b74a72f41d9
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageBuilder.Codeunit.al
@@ -0,0 +1,291 @@
+// ------------------------------------------------------------------------------------------------
+// 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
+ FREInvoiceProfileValidator: Codeunit "FR E-Invoice Profile Validator";
+ XmlDoc: XmlDocument;
+ RootElement: XmlElement;
+ AcknowledgementElement: XmlElement;
+ ReferenceElement: XmlElement;
+ StatusElement: XmlElement;
+ 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));
+ 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
+ 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));
+ 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":
+ 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
+ Error(UnsupportedMessageTypeErr, FREInvoiceMessage.Type);
+ end;
+ AcknowledgementElement.Add(ReferenceElement);
+ RootElement.Add(AcknowledgementElement);
+ XmlDoc.Add(RootElement);
+ FREInvoiceProfileValidator.Validate(XmlDoc, IsPPFMessage(FREInvoiceMessage));
+
+ TempBlob.CreateOutStream(OutStream, TextEncoding::UTF8);
+ 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";
+ 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;
+ 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 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";
+ 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;
+ 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;
+ RefusedStatusCodeTok: Label '210', Locked = true;
+ 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
new file mode 100644
index 00000000000..148f01c67bf
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageMgt.Codeunit.al
@@ -0,0 +1,463 @@
+// ------------------------------------------------------------------------------------------------
+// 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.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;
+
+codeunit 10975 "FR E-Invoice Message Mgt."
+{
+ Access = Internal;
+ InherentEntitlements = X;
+ InherentPermissions = X;
+
+ Permissions = tabledata "FR E-Invoice Message VAT" = ri;
+
+ 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])
+ begin
+ CheckBuyerResponseAllowed(EDocument);
+ 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")
+ var
+ EDocPaymentOccurrenceMgt: Codeunit "E-Doc. Payment Occurrence Mgt.";
+ begin
+ 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
+ EDocument.Get(EDocPaymentOccurrence."E-Document Entry No.");
+ if not ResolveEligibleFrenchService(EDocument) then
+ 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",
+ 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("Source Occurrence ID", OriginalOccurrence."Source Occurrence ID");
+ if not CollectedMessage.FindFirst() then
+ exit;
+
+ CreateAndSendMessage(
+ EDocument, "FR E-Invoice Message Type"::"Negative Collected", EDocPaymentOccurrence."Source Occurrence ID",
+ -CollectedMessage.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");
+ 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();
+ case MessageType of
+ MessageType::Collected:
+ FreezeSenderPlatform(EDocument, FREInvoiceMessage);
+ MessageType::"Negative Collected":
+ CopySenderPlatform(FREInvoiceMessage, OriginalEntryNo);
+ end;
+ 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);
+ FREInvoiceMessage.Modify();
+ EDocumentMessageAPI.QueueMessage(FREInvoiceMessage."E-Document Message Entry No.");
+ end;
+
+ 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);
+ 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)
+ 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";
+ 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")
+ 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;
+ GrossAmount: Decimal;
+ begin
+ 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
+ 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");
+
+ exit(GrossAmount);
+ 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 ResolveEligibleFrenchService(var EDocument: Record "E-Document"): Boolean
+ var
+ EDocumentService: Record "E-Document Service";
+ EDocumentServiceStatus: Record "E-Document Service Status";
+ begin
+ 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(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
+ 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";
+ 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
+ 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
+ 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';
+ 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/FREInvoiceMessageType.Enum.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.al
new file mode 100644
index 00000000000..66421e51470
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessageType.Enum.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;
+
+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';
+ }
+ 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/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/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..d02103e2586
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Core/FREInvoiceMessages.Page.al
@@ -0,0 +1,130 @@
+// ------------------------------------------------------------------------------------------------
+// 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("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.';
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
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/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/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/app/src/Extensions/EReportingEDocuments.PageExt.al b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/EReportingEDocuments.PageExt.al
index a6c47dd86a2..902bacb21d4 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
@@ -19,4 +19,58 @@ pageextension 10974 "E-Reporting E-Documents" extends "E-Documents"
}
}
}
+
+ actions
+ {
+ 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;
+ 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;
+ }
+ 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
new file mode 100644
index 00000000000..8585e6e06b4
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/app/src/Extensions/FREDocResponseType.EnumExt.al
@@ -0,0 +1,19 @@
+// ------------------------------------------------------------------------------------------------
+// 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(10970; Submitted)
+ {
+ Caption = 'Submitted';
+ }
+ 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/EDocFRStructImportTests.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/EDocFRStructImportTests.Codeunit.al
index af1abb67b3a..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
@@ -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;
@@ -25,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
@@ -74,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
@@ -97,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
@@ -118,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));
@@ -145,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
@@ -189,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/FREDocMsgSenderMock.Codeunit.al b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al
new file mode 100644
index 00000000000..2144f875407
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREDocMsgSenderMock.Codeunit.al
@@ -0,0 +1,88 @@
+// ------------------------------------------------------------------------------------------------
+// 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";
+ PayloadLine: Text;
+ InStream: InStream;
+ begin
+ SendCount += 1;
+ LastResponseType := MessageContext.GetResponseType();
+ TempBlob := MessageContext.GetTempBlob();
+ TempBlob.CreateInStream(InStream, TextEncoding::UTF8);
+ 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;
+
+ 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);
+ ReportSuccess := true;
+ SendCount := 0;
+ end;
+
+ procedure SetReportSuccess(NewReportSuccess: Boolean)
+ begin
+ ReportSuccess := NewReportSuccess;
+ 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;
+ 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
new file mode 100644
index 00000000000..fe75b9d3815
--- /dev/null
+++ b/src/Apps/FR/EDocument_FR/EReportingFR/test/src/FREInvoiceMessageTests.Codeunit.al
@@ -0,0 +1,1999 @@
+// ------------------------------------------------------------------------------------------------
+// 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.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;
+using Microsoft.Sales.History;
+using Microsoft.Sales.Receivables;
+using System.Utilities;
+
+codeunit 148151 "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 "E-Doc. Payment Occurrence" = rimd,
+ 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
+ Assert: Codeunit Assert;
+ LibraryERM: Codeunit "Library - ERM";
+ LibrarySales: Codeunit "Library - Sales";
+ MessageSenderMock: Codeunit "FR E-Doc. Msg. Sender Mock";
+
+ [Test]
+ 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.";
+ begin
+ // [FEATURE] [AI test]
+ // [SCENARIO] Applying a payment to an approved French E-Document creates a Collected message
+ Initialize();
+
+ // [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);
+ EDocPaymentOccurrence.SetRange("E-Document Entry No.", EDocument."Entry No");
+ EDocPaymentOccurrence.SetRange(Type, EDocPaymentOccurrence.Type::Applied);
+ 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');
+ 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
+ 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.";
+ 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");
+ 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);
+
+ // [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();
+ 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.');
+ AssertPayloadAmount(MessageSenderMock.GetLastPayload(), -120, 'EUR');
+ end;
+
+ [Test]
+ procedure RefusalSendsStatusAndReason()
+ var
+ 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.');
+ Assert.AreEqual("E-Doc. Response Type"::Refused, MessageSenderMock.GetLastResponseType(), 'The child message must be Refused.');
+ AssertPayloadStatus(MessageSenderMock.GetLastPayload(), '210');
+ AssertPayloadReasonCode(MessageSenderMock.GetLastPayload(), 'PRICE');
+ end;
+
+ [Test]
+ procedure RefusalWithoutReasonSendsStatusWithoutReasonElements()
+ var
+ EDocument: Record "E-Document";
+ FREInvoiceMessageMgt: Codeunit "FR E-Invoice Message Mgt.";
+ begin
+ // [FEATURE] [AI test]
+ // [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 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.');
+ 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
+ // [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);
+ FREInvoiceMessage.SetRange("E-Document Entry No.", EDocument."Entry No");
+ FREInvoiceMessage.SetRange(Type, FREInvoiceMessage.Type::Collected);
+ 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.');
+ 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]
+ procedure CollectedMessageAllowsMissingSenderPlatformIdentity()
+ 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 supports a service without optional sender-platform identity
+ Initialize();
+
+ // [GIVEN] An eligible payment whose service has no sender-platform ID
+ CreatePaymentScenarioWithoutSenderPlatform(EDocument, DetailedCustLedgEntry, "E-Document Service Status"::Approved);
+
+ // [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 CollectedMessageWithoutPlatformUsesCDVProfile()
+ 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 without platform identity retains the CDV profile
+ Initialize();
+
+ // [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);
+ 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
+ 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
+ CompanyInformation: Record "Company Information";
+ 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";
+ 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
+ // [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] VAT and sender-platform setup are changed after the original message
+ 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);
+ EDocumentService."FR Sender Platform ID" := 'CHANGED-PLATFORM';
+ 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);
+ 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.');
+ 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]
+ 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
+ EDocument: Record "E-Document";
+ EDocumentMessageAPI: Codeunit "E-Document Message API";
+ TempBlob: Codeunit "Temp Blob";
+ OutStream: OutStream;
+ ExternalDocumentID: Text[250];
+ ExternalMessageID: Text[250];
+ 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));
+ EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocumentID);
+ 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);
+ DuplicateMessageEntryNo := EDocumentMessageAPI.CreateIncomingMessage(
+ 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;
+
+ [Test]
+ procedure IncomingMessageRequiresRegisteredDocumentReference()
+ var
+ EDocument: Record "E-Document";
+ EDocumentMessageAPI: Codeunit "E-Document Message API";
+ 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;
+
+ [Test]
+ procedure BuyerAcceptanceQueuesAndSendsStatus205()
+ var
+ EDocument: Record "E-Document";
+ 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);
+ 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');
+ end;
+
+ [Test]
+ procedure BuyerResponseCannotBeRepeated()
+ var
+ 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();
+
+ // [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;
+
+ [Test]
+ procedure BuyerResponseCannotBeRepeatedAfterRefusal()
+ var
+ 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();
+
+ // [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;
+
+ [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
+ // [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);
+ ReceivedAt := CreateDateTime(20260101D, 120000T);
+ EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID);
+ 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.');
+ 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
+ // [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);
+ EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID);
+ ReceiveLifecycleMessage(EDocument, ExternalDocID, 'Submitted', '', '');
+ 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.');
+ 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
+ // [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);
+ 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'));
+
+ // [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.');
+ 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
+ // [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);
+ EDocumentMessageAPI.RegisterExternalDocumentReference(EDocument, EDocument.Service, ExternalDocID);
+ 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);
+ 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
+ // [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;
+
+ [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
+ // [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;
+
+ [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
+ // [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;
+
+ [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
+ // [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;
+
+ [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
+ // [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;
+
+ [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
+ EDocument: Record "E-Document";
+ FREInvoiceMessage: Record "FR E-Invoice Message";
+ 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";
+ FREInvoiceMessage.Type := FREInvoiceMessage.Type::Submitted;
+ FREInvoiceMessage."Source Occurrence ID" := CreateGuid();
+ FREInvoiceMessage."Event Date" := Today();
+ 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;
+
+ 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 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";
+ 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 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 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;
+ 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 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;
+ PartyPathTok: Label '//*[local-name()="ExchangedDocument"]/*[local-name()="%1"]', Locked = true;
+ begin
+ 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.');
+ 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";
+ FREInvoiceMessage: Record "FR E-Invoice Message";
+ GeneralLedgerSetup: Record "General Ledger Setup";
+ begin
+ EDocPaymentOccurrence.DeleteAll();
+ FREInvoiceMessage.DeleteAll();
+ MessageSenderMock.Reset();
+ EnsureService();
+ EnsureCompanyInformation();
+ GeneralLedgerSetup.Get();
+ if not GeneralLedgerSetup."Unrealized VAT" then begin
+ GeneralLedgerSetup."Unrealized VAT" := true;
+ GeneralLedgerSetup.Modify();
+ 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";
+ begin
+ 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")
+ 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 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"; ServiceStatus: Enum "E-Document Service Status")
+ begin
+ 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;
+ 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');
+ 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-MOCK';
+ 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);
+ 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);
+ 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');
+ 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;
+
+ 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";
+ begin
+ EDocumentServiceStatus.Init();
+ EDocumentServiceStatus."E-Document Entry No" := EDocument."Entry No";
+ EDocumentServiceStatus."E-Document Service Code" := EDocument.Service;
+ EDocumentServiceStatus.Status := ServiceStatus;
+ 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 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 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
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 e46e3ad628b..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
@@ -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;
@@ -34,7 +35,6 @@ codeunit 148148 "Factur-X CII XML Tests"
trigger OnRun()
begin
- // [FEATURE] [Factur-X FR E-document]
end;
var
@@ -185,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);
@@ -203,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);
@@ -451,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);
@@ -709,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);
@@ -939,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);
@@ -957,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);
@@ -1215,6 +1221,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";
@@ -1234,9 +1241,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.";
@@ -1257,6 +1274,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());
@@ -1313,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();
@@ -1353,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();
@@ -1953,6 +1972,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);
@@ -1989,6 +2009,10 @@ 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
@@ -2029,6 +2053,7 @@ 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);
@@ -2131,6 +2156,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);
@@ -2153,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;
@@ -2393,6 +2419,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;
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
diff --git a/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al b/src/Apps/W1/EDocument/App/Permissions/EDocCoreObjects.PermissionSet.al
index 782f758045d..1d09834768a 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,7 +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-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,
@@ -178,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/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..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,
@@ -42,6 +46,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/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
new file mode 100644
index 00000000000..698db3b63da
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Integration/Interfaces/IMessageSender.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;
+
+///
+/// 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 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 6a91bb64230..041f0603521 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,17 @@ 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, IMessageResponseHandler
{
Extensible = true;
Access = Public;
- DefaultImplementation = IConsentManager = "Consent Manager Default Impl.";
+ 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",
+ 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 c00103cff7c..ca180305fb7 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,16 @@ 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 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/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
new file mode 100644
index 00000000000..44443b859e0
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageContext.Codeunit.al
@@ -0,0 +1,86 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+ ///
+ /// 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 or Pending Response after successful transmission.
+ ///
+ /// The integration action status.
+ 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..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
@@ -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,232 @@ 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";
+ 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;
+ MessageSendingErrorInfo: ErrorInfo;
+ begin
+ EDocMessage.Get(MessageEntryNo);
+ EDocMessage.TestField(Direction, EDocMessage.Direction::Outgoing);
+ 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.");
+ EDocumentService.Get(EDocMessage.Service);
+ GetMessageBlob(MessageEntryNo, TempBlob);
+ if not TempBlob.HasValue() then
+ Error(MessagePayloadErr, MessageEntryNo);
+
+ EDocMessageContext.Initialize(EDocMessage, TempBlob);
+ MessageSender := EDocumentService."Service Integration V2";
+ MessageSender.SendMessage(EDocument, EDocumentService, EDocMessageContext);
+ 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());
+ Error(MessageSendingErrorInfo);
+ end;
+
+ EDocumentLog.InsertIntegrationLog(
+ EDocument, EDocumentService, EDocMessageContext.Http().GetHttpRequestMessage(), EDocMessageContext.Http().GetHttpResponseMessage());
+ 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)
+ 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();
+ Commit();
+ 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);
+ if EDocMessage.Status <> EDocMessage.Status::"Response Error" then
+ EDocMessage.TestField(Status, EDocMessage.Status::Error);
+ EDocMessage.TestField(Service);
+
+ if EDocMessage.Status = EDocMessage.Status::"Response Error" then
+ EDocMessage.Status := EDocMessage.Status::"Pending Response"
+ else
+ EDocMessage.Status := EDocMessage.Status::Queued;
+ EDocMessage.Modify();
+ Commit();
+ 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])
+ 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.LockTable();
+ 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
var
EDocDataStorage: Record "E-Doc. Data Storage";
@@ -96,4 +323,15 @@ 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.', 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.';
+ 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/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/EDocMessageSendJob.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al
new file mode 100644
index 00000000000..28f31c79d3d
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageSendJob.Codeunit.al
@@ -0,0 +1,38 @@
+// ------------------------------------------------------------------------------------------------
+// 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 Codeunit.Run(Codeunit::"E-Doc. Message Send Runner", EDocumentMessage) 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;
+
+ 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/App/src/Processing/Message/EDocMessageStatus.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al
index 62646edc3ba..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
@@ -19,4 +19,24 @@ enum 6429 "E-Doc. Message Status"
{
Caption = 'Sent';
}
+ value(2; Queued)
+ {
+ Caption = 'Queued';
+ }
+ value(3; Error)
+ {
+ Caption = 'Error';
+ }
+ value(4; Received)
+ {
+ 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
new file mode 100644
index 00000000000..a8f18bbfa9e
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMsgTransportDefault.Codeunit.al
@@ -0,0 +1,42 @@
+// ------------------------------------------------------------------------------------------------
+// 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, IMessageResponseHandler
+{
+ 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")
+ var
+ MessageTransportErrorInfo: ErrorInfo;
+ begin
+ MessageTransportErrorInfo.Message := StrSubstNo(MessageTransportNotSupportedErr, EDocumentService.Code);
+ MessageTransportErrorInfo.RecordId := EDocumentService.RecordId;
+ MessageTransportErrorInfo.PageNo := Page::"E-Document Service";
+ MessageTransportErrorInfo.AddNavigationAction(ShowEDocumentServiceLbl);
+ 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/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..5be2d3c7ffc
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocPaymentOccurrenceMgt.Codeunit.al
@@ -0,0 +1,153 @@
+// ------------------------------------------------------------------------------------------------
+// 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."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;
+ 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";
+ 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
+ CreateOccurrence(
+ 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(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.", EDocumentEntryNo);
+ EDocPaymentOccurrence.SetRange("Source Occurrence ID", SourceOccurrenceID);
+ EDocPaymentOccurrence.SetRange(Type, OccurrenceType);
+ if not EDocPaymentOccurrence.IsEmpty() then
+ exit;
+
+ EDocPaymentOccurrence.Init();
+ EDocPaymentOccurrence."E-Document Entry No." := EDocumentEntryNo;
+ 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.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");
+ 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..0400caaceb4
--- /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 6115 "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
new file mode 100644
index 00000000000..38bf5d304d6
--- /dev/null
+++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageAPI.Codeunit.al
@@ -0,0 +1,152 @@
+// ------------------------------------------------------------------------------------------------
+// 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;
+
+ ///
+ /// 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.";
+ begin
+ 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.
+ ///
+ /// 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;
+
+ ///
+ /// 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;
+
+ ///
+ /// 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.
+ ///
+ /// 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..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
@@ -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.';
+ }
}
}
}
@@ -61,6 +86,23 @@ page 6434 "E-Document Messages FactBox"
{
area(processing)
{
+ action(Retry)
+ {
+ ApplicationArea = Basic, Suite;
+ Caption = 'Retry';
+ ToolTip = 'Retry the failed message transmission or response polling operation 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;
@@ -87,7 +129,13 @@ page 6434 "E-Document Messages FactBox"
}
}
+ trigger OnAfterGetCurrRecord()
+ begin
+ RetryEnabled := (Rec.Direction = Rec.Direction::Outgoing) and (Rec.Status in [Rec.Status::Error, Rec.Status::"Response 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/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?';
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..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,9 +8,10 @@ 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
+codeunit 139658 "E-Doc. Integration Mock V2" implements IDocumentSender, IDocumentReceiver, IDocumentResponseHandler, ISentDocumentActions, IConsentManager, IMessageResponseHandler
{
Access = Internal;
@@ -37,6 +38,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
new file mode 100644
index 00000000000..0f0cb1081bf
--- /dev/null
+++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageMgtTests.Codeunit.al
@@ -0,0 +1,378 @@
+// ------------------------------------------------------------------------------------------------
+// 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 139893 "E-Doc. Message Mgt. Tests"
+{
+ Subtype = Test;
+ TestType = IntegrationTest;
+ TestPermissions = Disabled;
+
+ 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;
+
+ [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"::Unknown, "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;
+
+ [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"::Unknown, "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"::Unknown, "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"::Unknown, "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;
+
+ [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";
+ 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.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();
+
+ 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;
+
+ 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"::Unknown, "E-Document Direction"::Outgoing,
+ "E-Doc. Response Type"::None, TempBlob);
+ EDocMessage.Get(MessageEntryNo);
+ EDocMessage.Status := EDocMessage.Status::"Pending Response";
+ EDocMessage.Modify();
+ exit(MessageEntryNo);
+ end;
+}