diff --git a/src/Apps/W1/EDocument/App/app.json b/src/Apps/W1/EDocument/App/app.json index 3aa16d9bca..972589e59f 100644 --- a/src/Apps/W1/EDocument/App/app.json +++ b/src/Apps/W1/EDocument/App/app.json @@ -68,6 +68,10 @@ { "from": 6401, "to": 6410 + }, + { + "from": 6427, + "to": 6436 } ], "resourceExposurePolicy": { diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al b/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al index 3e2f8ea79b..1f97b1eb0f 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al @@ -11,6 +11,7 @@ using Microsoft.eServices.EDocument.OrderMatch; using Microsoft.eServices.EDocument.OrderMatch.Copilot; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.eServices.EDocument.Service; using Microsoft.Foundation.Attachment; using System.Telemetry; @@ -220,6 +221,12 @@ page 6121 "E-Document" Enabled = Rec.Direction = Rec.Direction::Outgoing; Visible = Rec.Direction = Rec.Direction::Outgoing; } + part(EDocMessages; "E-Document Messages FactBox") + { + Caption = 'Messages'; + SubPageLink = "E-Document Entry No." = field("Entry No"); + ShowFilter = false; + } } } actions @@ -305,6 +312,18 @@ page 6121 "E-Document" end end; } + action(RejectOrder) + { + Caption = 'Reject Order'; + ToolTip = 'Sends a rejection response to the sender of this inbound order.'; + Image = Reject; + Visible = IsIncomingDoc; + + trigger OnAction() + begin + EDocumentHelper.SendOrderRejection(Rec); + end; + } action(ViewFile) { ApplicationArea = Basic, Suite; diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al b/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al index 3b0390915b..c3d3b01a96 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocument.Table.al @@ -9,6 +9,7 @@ using Microsoft.eServices.EDocument.OrderMatch; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.Currency; using Microsoft.Foundation.Attachment; using Microsoft.Foundation.Reporting; @@ -416,6 +417,7 @@ table 6121 "E-Document" EDocumentIntegrationLog: Record "E-Document Integration Log"; EDocumentLog: Record "E-Document Log"; EDocImportedLine: Record "E-Doc. Imported Line"; + EDocumentMessage: Record "E-Document Message"; EDocumentServiceStatus: Record "E-Document Service Status"; #if not CLEAN27 PurchaseHeader: Record "Purchase Header"; @@ -453,6 +455,10 @@ table 6121 "E-Document" if not EDocImportedLine.IsEmpty() then EDocImportedLine.DeleteAll(true); + EDocumentMessage.SetRange("E-Document Entry No.", Rec."Entry No"); + if not EDocumentMessage.IsEmpty() then + EDocumentMessage.DeleteAll(true); + #if not CLEAN27 // Version 1 processing cleanup // Can be removed soon as version 1 is fully migrated to version 2 diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocuments.Page.al b/src/Apps/W1/EDocument/App/src/Document/EDocuments.Page.al index 1fa1018045..e0876718ef 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocuments.Page.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocuments.Page.al @@ -4,6 +4,10 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.eServices.EDocument.Service; +using Microsoft.Foundation.Attachment; + page 6122 "E-Documents" { ApplicationArea = Basic, Suite; @@ -61,6 +65,39 @@ page 6122 "E-Documents" } } } + area(FactBoxes) + { + part("Attached Documents List"; "Doc. Attachment List Factbox") + { + ApplicationArea = All; + Caption = 'Documents'; + UpdatePropagation = Both; + SubPageLink = "E-Document Entry No." = field("Entry No"), + "E-Document Attachment" = const(true); + } + part(InboundEDocFactbox; "Inbound E-Doc. Factbox") + { + Caption = 'Details'; + SubPageLink = "E-Document Entry No" = field("Entry No"); + ShowFilter = false; + Enabled = Rec.Direction = Rec.Direction::Incoming; + Visible = Rec.Direction = Rec.Direction::Incoming; + } + part("Outbound E-Doc. Factbox"; "Outbound E-Doc. Factbox") + { + Caption = 'Details'; + SubPageLink = "E-Document Entry No" = field("Entry No"); + ShowFilter = false; + Enabled = Rec.Direction = Rec.Direction::Outgoing; + Visible = Rec.Direction = Rec.Direction::Outgoing; + } + part(EDocMessages; "E-Document Messages FactBox") + { + Caption = 'Messages'; + SubPageLink = "E-Document Entry No." = field("Entry No"); + ShowFilter = false; + } + } } actions { @@ -138,9 +175,14 @@ page 6122 "E-Documents" EDocImport: Codeunit "E-Doc. Import"; begin EDocImport.UploadDocument(EDocument); - if EDocument."Entry No" <> 0 then begin - EDocImport.ProcessIncomingEDocument(EDocument, EDocument.GetEDocumentService().GetDefaultImportParameters()); + if EDocument."Entry No" = 0 then + exit; + if EDocument.Direction = EDocument.Direction::Outgoing then begin + // File was classified as a message (e.g. OrderResponse) linked to this outbound document. Page.Run(Page::"E-Document", EDocument); + exit; end; + EDocImport.ProcessIncomingEDocument(EDocument, EDocument.GetEDocumentService().GetDefaultImportParameters()); + Page.Run(Page::"E-Document", EDocument); end; } diff --git a/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al b/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al index 1c35a24fb2..79741a7e6a 100644 --- a/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al +++ b/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al @@ -6,6 +6,8 @@ namespace Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.EServices.EDocument.Processing.Import.Sales; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Foundation.Attachment; using Microsoft.Purchases.Vendor; using System.Agents; @@ -187,6 +189,12 @@ page 6105 "Inbound E-Documents" SubPageLink = "E-Document Entry No" = field("Entry No"); ShowFilter = false; } + part(EDocMessages; "E-Document Messages FactBox") + { + Caption = 'Messages'; + SubPageLink = "E-Document Entry No." = field("Entry No"); + ShowFilter = false; + } } } actions diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al index c2d3853933..b0ca178628 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al @@ -196,11 +196,19 @@ codeunit 6140 "E-Doc. Import" InStr: InStream; FileName: Text; EDocumentServiceStatus: Enum "E-Document Service Status"; + Handled: Boolean; begin - if Page.RunModal(Page::"E-Document Services", EDocumentService) <> Action::LookupOK then + if not UploadIntoStream('', '', '', FileName, InStr) then exit; - if not UploadIntoStream('', '', '', FileName, InStr) then + OutStr := TempBlob.CreateOutStream(); + CopyStream(OutStr, InStr); + + OnAfterUploadFile(TempBlob, FileName, EDocument, Handled); + if Handled then + exit; + + if Page.RunModal(Page::"E-Document Services", EDocumentService) <> Action::LookupOK then exit; EDocument.Direction := EDocument.Direction::Incoming; @@ -208,9 +216,6 @@ codeunit 6140 "E-Doc. Import" EDocument.Service := EDocumentService.Code; EDocumentServiceStatus := "E-Document Service Status"::Imported; - OutStr := TempBlob.CreateOutStream(); - CopyStream(OutStr, InStr); - EDocument."File Name" := CopyStr(FileName, 1, 256); if EDocument."Entry No" = 0 then begin @@ -226,6 +231,11 @@ codeunit 6140 "E-Doc. Import" EDocument.Modify(); end; + [IntegrationEvent(false, false)] + local procedure OnAfterUploadFile(var TempBlob: Codeunit "Temp Blob"; FileName: Text; var EDocument: Record "E-Document"; var Handled: Boolean) + begin + end; + internal procedure V1_GetBasicInfo(var EDocument: Record "E-Document") var EDocService: Record "E-Document Service"; diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentProcessing.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentProcessing.Codeunit.al index 759e43edc2..ac24fdb398 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentProcessing.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentProcessing.Codeunit.al @@ -6,11 +6,14 @@ namespace Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; using Microsoft.Foundation.Reporting; using Microsoft.Inventory.Location; using Microsoft.Inventory.Transfer; +using Microsoft.Peppol.Response; using Microsoft.Purchases.Document; using Microsoft.Purchases.History; using Microsoft.Purchases.Vendor; @@ -744,6 +747,20 @@ codeunit 6108 "E-Document Processing" exit(false); end; + procedure SendOrderRejection(EDocument: Record "E-Document") + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + ResponseBlob: Codeunit "Temp Blob"; + IResponseBuilder: Interface IOrderResponseBuilder; + begin + EDocument.TestField(Direction, EDocument.Direction::Incoming); + IResponseBuilder := EDocument."Read into Draft Impl."; + if not IResponseBuilder.SupportsOrderResponse(EDocument) then + exit; + IResponseBuilder.BuildOrderResponse(EDocument, "E-Doc. Response Type"::Rejected, ResponseBlob); + EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Outgoing, "E-Doc. Response Type"::Rejected, ResponseBlob); + end; + [IntegrationEvent(false, false)] local procedure OnAfterGetTypeFromSourceDocument(RecordVariant: Variant; var EDocumentType: Enum "E-Document Type") begin diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al index 650193f4dc..513436f979 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -10,6 +10,8 @@ using Microsoft.eServices.EDocument.OrderMatch; using Microsoft.EServices.EDocument.Processing; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; using Microsoft.eServices.EDocument.Service.Participant; using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; @@ -17,6 +19,7 @@ using Microsoft.Finance.GeneralLedger.Posting; using Microsoft.Finance.VAT.Setup; using Microsoft.Foundation.Reporting; using Microsoft.Inventory.Transfer; +using Microsoft.Peppol.Response; using Microsoft.Purchases.Document; using Microsoft.Purchases.History; using Microsoft.Purchases.Posting; @@ -744,4 +747,27 @@ codeunit 6103 "E-Document Subscribers" local procedure OnAfterUpdateToPostedPurchaseEDocument(var EDocument: Record "E-Document"; PostedRecord: Variant; PostedDocumentNo: Code[20]; DocumentType: Enum "E-Document Type") begin end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Release Sales Document", 'OnAfterReleaseSalesDoc', '', false, false)] + local procedure OnAfterReleaseSalesDoc(var SalesHeader: Record "Sales Header"; PreviewMode: Boolean; var LinesWereModified: Boolean; SkipWhseRequestOperations: Boolean) + var + EDocument: Record "E-Document"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + ResponseBlob: Codeunit "Temp Blob"; + SalesHeaderRef: RecordRef; + IResponseBuilder: Interface IOrderResponseBuilder; + begin + if PreviewMode then + exit; + SalesHeaderRef.GetTable(SalesHeader); + EDocument.SetRange("Document Record ID", SalesHeaderRef.RecordId); + EDocument.SetRange(Direction, EDocument.Direction::Incoming); + if not EDocument.FindLast() then + exit; + IResponseBuilder := EDocument."Read into Draft Impl."; + if not IResponseBuilder.SupportsOrderResponse(EDocument) then + exit; + IResponseBuilder.BuildOrderResponse(EDocument, "E-Doc. Response Type"::Accepted, ResponseBlob); + EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Outgoing, "E-Doc. Response Type"::Accepted, ResponseBlob); + end; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocEmptyDraft.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocEmptyDraft.Codeunit.al index fb845be46d..ad08b5f3c4 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocEmptyDraft.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocEmptyDraft.Codeunit.al @@ -51,4 +51,5 @@ codeunit 6193 "E-Doc. Empty Draft" implements IStructureReceivedEDocument, IStru begin Error(NoDataErr); end; + } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al index 9b62c4ab3f..363c4e3f7e 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; /// /// Enum for E-Document Processing @@ -33,4 +34,14 @@ enum 6107 "E-Doc. Process Draft" implements IProcessStructuredData Caption = 'Purchase Credit Memo'; Implementation = IProcessStructuredData = "EDoc Prepare Cr. Memo Draft"; } + value(3; "Sales Order") + { + Caption = 'Sales Order'; + Implementation = IProcessStructuredData = "Prepare Sales E-Doc. Draft"; + } + value(4; "E-Document Message") + { + Caption = 'E-Document Message'; + Implementation = IProcessStructuredData = "E-Doc. Message Draft Handler"; + } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al index ce558f9aff..a3ae458d46 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al @@ -10,10 +10,11 @@ using Microsoft.eServices.EDocument.Processing.Interfaces; /// /// Specifies how a structured data type should be interpreted and read into a draft. /// -enum 6109 "E-Doc. Read into Draft" implements IStructuredFormatReader +enum 6109 "E-Doc. Read into Draft" implements IStructuredFormatReader, IOrderResponseBuilder { Extensible = true; - DefaultImplementation = IStructuredFormatReader = "E-Doc. Unspecified Impl."; + DefaultImplementation = IStructuredFormatReader = "E-Doc. Unspecified Impl.", + IOrderResponseBuilder = "E-Doc. Unspecified Impl."; value(0; "Unspecified") { @@ -33,7 +34,8 @@ enum 6109 "E-Doc. Read into Draft" implements IStructuredFormatReader value(3; "PEPPOL") { Caption = 'PEPPOL'; - Implementation = IStructuredFormatReader = "E-Document PEPPOL Handler"; + Implementation = IStructuredFormatReader = "E-Document PEPPOL Handler", + IOrderResponseBuilder = "E-Document PEPPOL Handler"; } value(4; "MLLM") { diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocUnspecifiedImpl.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocUnspecifiedImpl.Codeunit.al index a47c0c0896..849c2bf3c9 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocUnspecifiedImpl.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocUnspecifiedImpl.Codeunit.al @@ -6,12 +6,13 @@ namespace Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.Peppol.Response; using System.Utilities; /// /// Default implementations for E-Document interfaces. /// -codeunit 6116 "E-Doc. Unspecified Impl." implements IStructureReceivedEDocument, IEDocumentFinishDraft, IStructuredFormatReader, IEDocFileFormat +codeunit 6116 "E-Doc. Unspecified Impl." implements IStructureReceivedEDocument, IEDocumentFinishDraft, IStructuredFormatReader, IOrderResponseBuilder, IEDocFileFormat { Access = Internal; InherentEntitlements = X; @@ -46,6 +47,15 @@ codeunit 6116 "E-Doc. Unspecified Impl." implements IStructureReceivedEDocument, Error(EDocumentNoReadSpecifiedErr); end; + procedure SupportsOrderResponse(EDocument: Record "E-Document"): Boolean + begin + exit(false); + end; + + procedure BuildOrderResponse(EDocument: Record "E-Document"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob") + begin + end; + procedure FileExtension(): Text begin end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al index f7ba514c45..c836db8003 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al @@ -7,6 +7,8 @@ namespace Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Peppol.Response; using Microsoft.Purchases.Vendor; using System.IO; using System.Utilities; @@ -118,8 +120,11 @@ codeunit 6104 "Import E-Document Process" local procedure ReadIntoDraft(EDocument: Record "E-Document") var EDocumentDataStorage: Record "E-Doc. Data Storage"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; FromBlob: Codeunit "Temp Blob"; + ResponseBlob: Codeunit "Temp Blob"; IStructuredFormatReader: Interface IStructuredFormatReader; + IResponseBuilder: Interface IOrderResponseBuilder; begin if EDocumentDataStorage.Get(EDocument."Structured Data Entry No.") then FromBlob := EDocumentDataStorage.GetTempBlob(); @@ -131,6 +136,12 @@ codeunit 6104 "Import E-Document Process" EDocument."Process Draft Impl." := IStructuredFormatReader.ReadIntoDraft(EDocument, FromBlob); EDocument.Modify(); + + IResponseBuilder := EDocument."Read into Draft Impl."; + if IResponseBuilder.SupportsOrderResponse(EDocument) then begin + IResponseBuilder.BuildOrderResponse(EDocument, "E-Doc. Response Type"::Acknowledged, ResponseBlob); + EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Outgoing, "E-Doc. Response Type"::Acknowledged, ResponseBlob); + end; end; local procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters") @@ -164,6 +175,9 @@ codeunit 6104 "Import E-Document Process" var IEDocumentFinishDraft: Interface IEDocumentFinishDraft; begin + if EDocument."Document Type" = "E-Document Type"::None then + exit; + IEDocumentFinishDraft := EDocument."Document Type"; // Clean up / reset E-Document fields diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al index ddc548d7f5..f9c5bbe74b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al @@ -238,4 +238,5 @@ codeunit 6174 "E-Document ADI Handler" implements IStructureReceivedEDocument, I exit(-1); // Signal parse failure end; #pragma warning restore AA0139 + } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al index 15f83d9e68..43fd4c4f9d 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al @@ -328,4 +328,5 @@ codeunit 6231 "E-Document MLLM Handler" implements IStructureReceivedEDocument, if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document MLLM Analysis") then CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis", ''); end; + } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al index f7d82d018e..48ea3ad741 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al @@ -8,6 +8,8 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.eServices.EDocument.Processing.Message; +using Microsoft.Peppol.Response; using System.Utilities; /// @@ -17,7 +19,7 @@ using System.Utilities; /// Spec reference: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/tree/ /// https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-creditnote/tree/ /// -codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader +codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader, IOrderResponseBuilder { Access = Internal; InherentEntitlements = X; @@ -34,12 +36,16 @@ codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootElement: XmlElement; + OrderNamespaceLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:Order-2', Locked = true; + OrderResponseNamespaceLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:OrderResponse-2', Locked = true; begin EDocumentPurchaseHeader.InsertForEDocument(EDocument); TempBlob.CreateInStream(DocStream, TextEncoding::UTF8); XmlDocument.ReadFrom(DocStream, PeppolXML); PeppolUtility.InitializePEPPOL3Namespaces(XmlNamespaces); + XmlNamespaces.AddNamespace('order', OrderNamespaceLbl); + XmlNamespaces.AddNamespace('resp', OrderResponseNamespaceLbl); PeppolXML.GetRoot(RootElement); case UpperCase(RootElement.LocalName()) of @@ -65,7 +71,11 @@ codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader 'INVOICE': exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); 'CREDITNOTE': - exit(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo"); + exit(ProcessCreditNote(EDocument, PeppolXML, XmlNamespaces)); + 'ORDER': + exit(ProcessOrder(EDocument, PeppolXML, XmlNamespaces)); + 'ORDERRESPONSE': + exit(HandleInboundOrderResponse(EDocument, TempBlob, PeppolXML, XmlNamespaces)); else exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); end; @@ -220,4 +230,53 @@ codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader Error('A view is not implemented for this handler.'); end; + procedure SupportsOrderResponse(EDocument: Record "E-Document"): Boolean + begin + exit(EDocument."Process Draft Impl." = "E-Doc. Process Draft"::"Sales Order"); + end; + + procedure BuildOrderResponse(EDocument: Record "E-Document"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob") + var + EDocSalesHeader: Record "E-Document Sales Header"; + Builder: Codeunit "PEPPOL Order Resp. Builder"; + begin + EDocSalesHeader.GetFromEDocument(EDocument); + Builder.Build(EDocSalesHeader."E-Document Entry No.", EDocSalesHeader."Buyer Order No.", EDocSalesHeader."Seller Company Name", EDocSalesHeader."Buyer Company Name", ResponseType, TempBlob); + end; + + local procedure HandleInboundOrderResponse(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"; PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager): Enum "E-Doc. Process Draft" + var + OutboundEDocument: Record "E-Document"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + ResponseCode: Text; + OrderRefId: Text; + ResponseType: Enum "E-Doc. Response Type"; + begin + PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/resp:OrderResponse/cbc:OrderResponseCode', ResponseCode); + PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/resp:OrderResponse/cac:OrderReference/cbc:ID', OrderRefId); + ResponseType := CodeToResponseType(ResponseCode); + + OutboundEDocument.SetRange("Document No.", CopyStr(OrderRefId, 1, MaxStrLen(OutboundEDocument."Document No."))); + OutboundEDocument.SetRange(Direction, OutboundEDocument.Direction::Outgoing); + if OutboundEDocument.FindLast() then + EDocMessageMgt.CreateMessage(OutboundEDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Incoming, ResponseType, TempBlob) + else + EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Incoming, ResponseType, TempBlob); + + exit("E-Doc. Process Draft"::"E-Document Message"); + end; + + local procedure CodeToResponseType(ResponseCode: Text): Enum "E-Doc. Response Type" + begin + case UpperCase(ResponseCode) of + 'AB': + exit("E-Doc. Response Type"::Acknowledged); + 'AC', 'AP': + exit("E-Doc. Response Type"::Accepted); + 'RE': + exit("E-Doc. Response Type"::Rejected); + else + exit("E-Doc. Response Type"::None); + end; + end; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IOrderResponseBuilder.Interface.al b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IOrderResponseBuilder.Interface.al new file mode 100644 index 0000000000..d8d4791081 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IOrderResponseBuilder.Interface.al @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Interfaces; + +using Microsoft.eServices.EDocument; +using Microsoft.Peppol.Response; +using System.Utilities; + +/// +/// Allows a format reader to build an outbound order response for a received e-document. +/// Separate from IStructuredFormatReader to avoid a breaking change on that interface. +/// +interface IOrderResponseBuilder +{ + + /// + /// Returns whether this format reader expects an order response to be sent back to the sender for the given E-Document. + /// Format apps that generate outbound response messages (e.g. PEPPOL Order Response) implement this to signal eligibility. + /// EDocument."Process Draft Impl." is already set when this is called, so implementations can inspect the draft type. + /// + /// The E-Document record with "Process Draft Impl." populated. + /// True if an order response should be generated; false otherwise. + procedure SupportsOrderResponse(EDocument: Record "E-Document"): Boolean; + + /// + /// Builds a format-specific order response XML blob into TempBlob. + /// Called by the framework after ReadIntoDraft (for AB) and after Sales Order release (for AC). + /// Implementations that return false from SupportsOrderResponse may leave TempBlob empty. + /// + procedure BuildOrderResponse(EDocument: Record "E-Document"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob"); + +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IStructuredFormatReader.Interface.al b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IStructuredFormatReader.Interface.al index 219c281c6f..fde42ac673 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IStructuredFormatReader.Interface.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IStructuredFormatReader.Interface.al @@ -25,7 +25,7 @@ interface IStructuredFormatReader /// - /// Presents a view of the data + /// Presents a view of the data /// /// The E-Document record. /// The temporary blob that contains the data to read diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageDraftHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageDraftHandler.Codeunit.al new file mode 100644 index 0000000000..17924dd4b2 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageDraftHandler.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.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.Purchases.Vendor; + +/// +/// No-op IProcessStructuredData implementation for inbound E-Document messages (e.g. PEPPOL Order Response). +/// When ReadIntoDraft returns "E-Document Message", the pipeline calls PrepareDraft which returns None +/// so that FinishDraft exits early without creating a BC document. +/// +codeunit 6435 "E-Doc. Message Draft Handler" implements IProcessStructuredData +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): Enum "E-Document Type" + begin + exit("E-Document Type"::None); + end; + + procedure GetVendor(EDocument: Record "E-Document"; Customizations: Enum "E-Doc. Proc. Customizations"): Record Vendor + var + EmptyVendor: Record Vendor; + begin + exit(EmptyVendor); + end; + + procedure OpenDraftPage(var EDocument: Record "E-Document") + begin + end; + + procedure CleanUpDraft(EDocument: Record "E-Document") + begin + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al new file mode 100644 index 0000000000..35e019958a --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageMgt.Codeunit.al @@ -0,0 +1,99 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Peppol.Response; +using System.Utilities; + +/// +/// Public API for creating and reading E-Document messages. +/// Format apps call CreateMessage to store a response/message blob linked to an E-Document. +/// +codeunit 6433 "E-Doc. Message Mgt." +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + Permissions = + tabledata "E-Document Message" = rim, + tabledata "E-Doc. Data Storage" = rim; + + /// + /// Creates an E-Document message record and stores the XML payload blob. + /// Returns the Entry No. of the new message row. + /// + procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; var TempBlob: Codeunit "Temp Blob"): Integer + begin + exit(CreateMessage(EDocument, MessageType, "E-Document Direction"::Outgoing, "E-Doc. Response Type"::None, TempBlob)); + end; + + /// + /// Creates an E-Document message record with an explicit direction and stores the XML payload blob. + /// Returns the Entry No. of the new message row. + /// + procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; Direction: Enum "E-Document Direction"; var TempBlob: Codeunit "Temp Blob"): Integer + begin + exit(CreateMessage(EDocument, MessageType, Direction, "E-Doc. Response Type"::None, TempBlob)); + end; + + /// + /// Creates an E-Document message record with an explicit direction and response type, and stores the XML payload blob. + /// Returns the Entry No. of the new message row. + /// + procedure CreateMessage(EDocument: Record "E-Document"; MessageType: Enum "E-Document Message Type"; Direction: Enum "E-Document Direction"; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob"): Integer + var + EDocMessage: Record "E-Document Message"; + DataStorageEntryNo: Integer; + begin + DataStorageEntryNo := InsertDataStorage(TempBlob); + + EDocMessage.Init(); + EDocMessage."E-Document Entry No." := EDocument."Entry No"; + EDocMessage."Message Type" := MessageType; + EDocMessage.Direction := Direction; + EDocMessage."Response Type" := ResponseType; + EDocMessage.Status := EDocMessage.Status::Created; + EDocMessage.Service := EDocument.Service; + EDocMessage."Data Storage Entry No." := DataStorageEntryNo; + EDocMessage."Created At" := CurrentDateTime(); + EDocMessage.Insert(); + exit(EDocMessage."Entry No."); + end; + + /// + /// Loads the payload blob for the given message entry number into TempBlob. + /// + procedure GetMessageBlob(MessageEntryNo: Integer; var TempBlob: Codeunit "Temp Blob") + var + EDocMessage: Record "E-Document Message"; + EDocDataStorage: Record "E-Doc. Data Storage"; + begin + if not EDocMessage.Get(MessageEntryNo) then + exit; + if not EDocDataStorage.Get(EDocMessage."Data Storage Entry No.") then + exit; + TempBlob := EDocDataStorage.GetTempBlob(); + end; + + local procedure InsertDataStorage(TempBlob: Codeunit "Temp Blob"): Integer + var + EDocDataStorage: Record "E-Doc. Data Storage"; + EDocRecRef: RecordRef; + begin + if not TempBlob.HasValue() then + exit(0); + + EDocDataStorage.Init(); + EDocDataStorage.Insert(); + EDocDataStorage.Name := ''; + EDocDataStorage."Data Storage Size" := TempBlob.Length(); + EDocRecRef.GetTable(EDocDataStorage); + TempBlob.ToRecordRef(EDocRecRef, EDocDataStorage.FieldNo("Data Storage")); + EDocRecRef.Modify(); + exit(EDocDataStorage."Entry No."); + end; +} 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 new file mode 100644 index 0000000000..cd8140260d --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocMessageStatus.Enum.al @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Message; + +/// +/// Operational status of an E-Document message record. +/// +enum 6431 "E-Doc. Message Status" +{ + Extensible = true; + + value(0; Created) + { + Caption = 'Created'; + } + value(1; Sent) + { + Caption = 'Sent'; + } +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocResponseTypeExt.EnumExt.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocResponseTypeExt.EnumExt.al new file mode 100644 index 0000000000..1d9ef286ad --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocResponseTypeExt.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.Processing.Message; + +using Microsoft.Peppol.Response; + +enumextension 6410 "E-Doc. Response Type Core Ext" extends "E-Doc. Response Type" +{ + value(0; None) + { + Caption = 'None'; + } +} 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 new file mode 100644 index 0000000000..ce19e2939f --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessage.Table.al @@ -0,0 +1,85 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Peppol.Response; + +/// +/// Stores a message (e.g. a PEPPOL Order Response) that relates to an existing E-Document. +/// A message does not produce a BC document; it updates the lifecycle state of the parent E-Document. +/// +table 6432 "E-Document Message" +{ + Access = Internal; + Caption = 'E-Document Message'; + ReplicateData = false; + InherentEntitlements = RIMDX; + InherentPermissions = RIMDX; + + fields + { + field(1; "Entry No."; Integer) + { + Caption = 'Entry No.'; + AutoIncrement = true; + DataClassification = SystemMetadata; + } + field(2; "E-Document Entry No."; Integer) + { + Caption = 'E-Document Entry No.'; + TableRelation = "E-Document"."Entry No"; + DataClassification = SystemMetadata; + } + field(3; "Message Type"; Enum "E-Document Message Type") + { + Caption = 'Message Type'; + DataClassification = SystemMetadata; + } + field(4; Direction; Enum "E-Document Direction") + { + Caption = 'Direction'; + DataClassification = SystemMetadata; + } + field(5; Status; Enum "E-Doc. Message Status") + { + Caption = 'Status'; + DataClassification = SystemMetadata; + } + field(6; "Data Storage Entry No."; Integer) + { + Caption = 'Data Storage Entry No.'; + TableRelation = "E-Doc. Data Storage"."Entry No."; + DataClassification = SystemMetadata; + } + field(7; "Created At"; DateTime) + { + Caption = 'Created At'; + DataClassification = SystemMetadata; + } + field(8; "Response Type"; Enum "E-Doc. Response Type") + { + Caption = 'Response Type'; + DataClassification = SystemMetadata; + } + field(9; Service; Code[20]) + { + Caption = 'Service'; + TableRelation = "E-Document Service"; + DataClassification = SystemMetadata; + } + } + + keys + { + key(PK; "Entry No.") + { + Clustered = true; + } + key(EDocument; "E-Document Entry No.") + { + } + } +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageTypeExt.EnumExt.al b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageTypeExt.EnumExt.al new file mode 100644 index 0000000000..d86c1a5995 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessageTypeExt.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.Processing.Message; + +using Microsoft.Peppol.Response; + +enumextension 6430 "E-Doc. Message Type Core Ext" extends "E-Document Message Type" +{ + value(0; Unknown) + { + Caption = 'Unknown'; + } +} 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 new file mode 100644 index 0000000000..809410f873 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Message/EDocumentMessagesFactBox.Page.al @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Utilities; + +/// +/// Factbox listpart showing all messages related to an E-Document. +/// Surfaced on the E-Document card page via SubPageLink on "E-Document Entry No.". +/// +page 6434 "E-Document Messages FactBox" +{ + ApplicationArea = Basic, Suite; + Caption = 'Messages'; + PageType = ListPart; + SourceTable = "E-Document Message"; + Editable = false; + InsertAllowed = false; + DeleteAllowed = false; + ModifyAllowed = false; + + layout + { + area(content) + { + repeater(Messages) + { + field("Message Type"; Rec."Message Type") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the type of the message, for example PEPPOL Order Response.'; + } + field(Direction; Rec.Direction) + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies whether the message was sent (Outgoing) or received (Incoming).'; + } + field(Status; Rec.Status) + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies the current status of the message.'; + } + field("Created At"; Rec."Created At") + { + ApplicationArea = Basic, Suite; + ToolTip = 'Specifies when the message was created.'; + } + } + } + } + + actions + { + area(processing) + { + action(ViewXML) + { + ApplicationArea = Basic, Suite; + Caption = 'View XML'; + ToolTip = 'Download the raw XML payload of this message.'; + Image = XMLFile; + Scope = Repeater; + + trigger OnAction() + var + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + TempBlob: Codeunit "Temp Blob"; + InStr: InStream; + FileName: Text; + begin + EDocMessageMgt.GetMessageBlob(Rec."Entry No.", TempBlob); + if not TempBlob.HasValue() then + exit; + TempBlob.CreateInStream(InStr); + FileName := BuildFileName(); + DownloadFromStream(InStr, '', '', '', FileName); + end; + } + } + } + + var + FileNameTok: Label 'E-Document_%1_Response_%2.xml', Comment = '%1 = E-Document number, %2 = human-readable response type', Locked = true; + + local procedure BuildFileName(): Text + var + ResponseTypeText: Text; + begin + if Rec."Response Type" = Rec."Response Type"::None then + ResponseTypeText := Format(Rec."Message Type") + else + ResponseTypeText := Format(Rec."Response Type"); + exit(StrSubstNo(FileNameTok, Rec."E-Document Entry No.", ResponseTypeText)); + end; +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al new file mode 100644 index 0000000000..dea420da6d --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMessageResponseTests.Codeunit.al @@ -0,0 +1,126 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Peppol.Response; +using Microsoft.Sales.Customer; +using System.Utilities; + +codeunit 139898 "E-Doc. Message Response Tests" +{ + Subtype = Test; + TestType = IntegrationTest; + TestPermissions = Disabled; + + var + EDocumentService: Record "E-Document Service"; + Assert: Codeunit Assert; + LibraryEDoc: Codeunit "Library - E-Document"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + IsInitialized: Boolean; + + [Test] + procedure CreateAcknowledgedMessage() + var + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + PEPPOLRespBuilder: Codeunit "PEPPOL Order Resp. Builder"; + TempBlob: Codeunit "Temp Blob"; + MessageEntryNo: Integer; + begin + Initialize(); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + + PEPPOLRespBuilder.Build(EDocument."Entry No", 'PO-001', 'Seller Corp.', 'Buyer Inc.', "E-Doc. Response Type"::Acknowledged, TempBlob); + MessageEntryNo := EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Incoming, "E-Doc. Response Type"::Acknowledged, TempBlob); + + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual("E-Doc. Response Type"::Acknowledged, EDocMessage."Response Type", 'Response type must be Acknowledged.'); + Assert.AreEqual("E-Document Message Type"::"PEPPOL Order Response", EDocMessage."Message Type", 'Message type must be PEPPOL Order Response.'); + Assert.AreEqual("E-Document Direction"::Incoming, EDocMessage.Direction, 'Direction must be Incoming.'); + Assert.AreEqual(EDocument."Entry No", EDocMessage."E-Document Entry No.", 'E-Document entry no. must match.'); + Assert.AreEqual("E-Doc. Message Status"::Created, EDocMessage.Status, 'Status must be Created.'); + Assert.IsTrue(EDocMessage."Data Storage Entry No." > 0, 'Data storage entry must be set.'); + end; + + [Test] + procedure CreateAcceptedMessage() + var + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + PEPPOLRespBuilder: Codeunit "PEPPOL Order Resp. Builder"; + TempBlob: Codeunit "Temp Blob"; + MessageEntryNo: Integer; + begin + Initialize(); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + + PEPPOLRespBuilder.Build(EDocument."Entry No", 'PO-002', 'Seller Corp.', 'Buyer Inc.', "E-Doc. Response Type"::Accepted, TempBlob); + MessageEntryNo := EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Incoming, "E-Doc. Response Type"::Accepted, TempBlob); + + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual("E-Doc. Response Type"::Accepted, EDocMessage."Response Type", 'Response type must be Accepted.'); + Assert.AreEqual("E-Document Message Type"::"PEPPOL Order Response", EDocMessage."Message Type", 'Message type must be PEPPOL Order Response.'); + Assert.AreEqual("E-Document Direction"::Incoming, EDocMessage.Direction, 'Direction must be Incoming.'); + Assert.AreEqual(EDocument."Entry No", EDocMessage."E-Document Entry No.", 'E-Document entry no. must match.'); + Assert.AreEqual("E-Doc. Message Status"::Created, EDocMessage.Status, 'Status must be Created.'); + Assert.IsTrue(EDocMessage."Data Storage Entry No." > 0, 'Data storage entry must be set.'); + end; + + [Test] + procedure CreateRejectedMessage() + var + EDocument: Record "E-Document"; + EDocMessage: Record "E-Document Message"; + EDocMessageMgt: Codeunit "E-Doc. Message Mgt."; + PEPPOLRespBuilder: Codeunit "PEPPOL Order Resp. Builder"; + TempBlob: Codeunit "Temp Blob"; + MessageEntryNo: Integer; + begin + Initialize(); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + + PEPPOLRespBuilder.Build(EDocument."Entry No", 'PO-003', 'Seller Corp.', 'Buyer Inc.', "E-Doc. Response Type"::Rejected, TempBlob); + MessageEntryNo := EDocMessageMgt.CreateMessage(EDocument, "E-Document Message Type"::"PEPPOL Order Response", "E-Document Direction"::Incoming, "E-Doc. Response Type"::Rejected, TempBlob); + + EDocMessage.Get(MessageEntryNo); + Assert.AreEqual("E-Doc. Response Type"::Rejected, EDocMessage."Response Type", 'Response type must be Rejected.'); + Assert.AreEqual("E-Document Message Type"::"PEPPOL Order Response", EDocMessage."Message Type", 'Message type must be PEPPOL Order Response.'); + Assert.AreEqual("E-Document Direction"::Incoming, EDocMessage.Direction, 'Direction must be Incoming.'); + Assert.AreEqual(EDocument."Entry No", EDocMessage."E-Document Entry No.", 'E-Document entry no. must match.'); + Assert.AreEqual("E-Doc. Message Status"::Created, EDocMessage.Status, 'Status must be Created.'); + Assert.IsTrue(EDocMessage."Data Storage Entry No." > 0, 'Data storage entry must be set.'); + end; + + local procedure Initialize() + var + EDocument: Record "E-Document"; + EDocumentServiceStatus: Record "E-Document Service Status"; + EDocMessage: Record "E-Document Message"; + EDocDataStorage: Record "E-Doc. Data Storage"; + Customer: Record Customer; + begin + LibraryLowerPermission.SetOutsideO365Scope(); + + EDocMessage.DeleteAll(); + EDocumentServiceStatus.DeleteAll(); + EDocument.DeleteAll(); + EDocDataStorage.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; +} diff --git a/src/Apps/W1/PEPPOL/App/src/Response/EDocResponseType.Enum.al b/src/Apps/W1/PEPPOL/App/src/Response/EDocResponseType.Enum.al new file mode 100644 index 0000000000..38406ccee3 --- /dev/null +++ b/src/Apps/W1/PEPPOL/App/src/Response/EDocResponseType.Enum.al @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Peppol.Response; + +/// +/// Human-readable response type carried by an E-Document message (e.g. a PEPPOL Order Response). +/// UNCL4343 OrderResponseCode: AB = Acknowledged, AC = Accepted, RE = Rejected. +/// Extend this enum in a format-specific app to declare additional response types. +/// +enum 37208 "E-Doc. Response Type" +{ + Extensible = true; + + value(37223; Acknowledged) + { + Caption = 'Acknowledged'; + } + value(37224; Accepted) + { + Caption = 'Accepted'; + } + value(37225; Rejected) + { + Caption = 'Rejected'; + } +} diff --git a/src/Apps/W1/PEPPOL/App/src/Response/EDocumentMessageType.Enum.al b/src/Apps/W1/PEPPOL/App/src/Response/EDocumentMessageType.Enum.al new file mode 100644 index 0000000000..ed406f05ea --- /dev/null +++ b/src/Apps/W1/PEPPOL/App/src/Response/EDocumentMessageType.Enum.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.Peppol.Response; + +/// +/// Identifies the type of an E-Document message (e.g. a PEPPOL Order Response). +/// Extend this enum in a format-specific app to declare new message types. +/// +enum 37207 "E-Document Message Type" +{ + Extensible = true; + + value(37210; "PEPPOL Order Response") + { + Caption = 'PEPPOL Order Response'; + } +} diff --git a/src/Apps/W1/PEPPOL/App/src/Response/PEPPOLOrderRespBuilder.Codeunit.al b/src/Apps/W1/PEPPOL/App/src/Response/PEPPOLOrderRespBuilder.Codeunit.al new file mode 100644 index 0000000000..3630c2fd1c --- /dev/null +++ b/src/Apps/W1/PEPPOL/App/src/Response/PEPPOLOrderRespBuilder.Codeunit.al @@ -0,0 +1,113 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.Peppol.Response; + +using System.Utilities; + +/// +/// Builds a minimal, well-formed PEPPOL BIS 28 Order Response (code AB = Acknowledged) +/// for an inbound Sales Order that was read into draft. +/// +codeunit 37209 "PEPPOL Order Resp. Builder" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + /// + /// Generates a minimal UBL OrderResponse XML blob using primitive parameters. + /// Writes the result into TempBlob. + /// + procedure Build(EDocEntryNo: Integer; BuyerOrderNo: Code[20]; SellerName: Text[100]; BuyerName: Text[100]; ResponseType: Enum "E-Doc. Response Type"; var TempBlob: Codeunit "Temp Blob") + var + XmlDoc: XmlDocument; + RootNode: XmlElement; + OutStr: OutStream; + begin + XmlDoc := XmlDocument.Create(); + XmlDoc.SetDeclaration(XmlDeclaration.Create('1.0', 'UTF-8', 'no')); + + RootNode := BuildOrderResponse(EDocEntryNo, BuyerOrderNo, SellerName, BuyerName, ResponseTypeToCode(ResponseType)); + XmlDoc.Add(RootNode); + + TempBlob.CreateOutStream(OutStr, TextEncoding::UTF8); + XmlDoc.WriteTo(OutStr); + end; + + /// + /// Maps a response type to its UNCL4343 OrderResponseCode used on the wire. + /// + local procedure ResponseTypeToCode(ResponseType: Enum "E-Doc. Response Type"): Code[10] + begin + case ResponseType of + "E-Doc. Response Type"::Acknowledged: + exit('AB'); + "E-Doc. Response Type"::Accepted: + exit('AC'); + "E-Doc. Response Type"::Rejected: + exit('RE'); + end; + end; + + local procedure BuildOrderResponse(EDocEntryNo: Integer; BuyerOrderNo: Code[20]; SellerName: Text[100]; BuyerName: Text[100]; ResponseCode: Code[10]) RootNode: XmlElement + var + OrderRefId: Text; + begin + RootNode := XmlElement.Create('OrderResponse', 'urn:oasis:names:specification:ubl:schema:xsd:OrderResponse-2'); + RootNode.Add(XmlAttribute.CreateNamespaceDeclaration('cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2')); + RootNode.Add(XmlAttribute.CreateNamespaceDeclaration('cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2')); + + // BIS 28 customization / profile + RootNode.Add(XmlElement.Create('CustomizationID', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + 'urn:fdc:peppol.eu:poacc:trns:order_response:3')); + RootNode.Add(XmlElement.Create('ProfileID', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + 'urn:fdc:peppol.eu:poacc:bis:order_only:3')); + + RootNode.Add(XmlElement.Create('ID', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + Format(EDocEntryNo))); + RootNode.Add(XmlElement.Create('IssueDate', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', + Format(Today(), 0, '--'))); + + RootNode.Add(XmlElement.Create('OrderResponseCode', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', ResponseCode)); + + // OrderReference + OrderRefId := BuyerOrderNo; + if OrderRefId = '' then + OrderRefId := Format(EDocEntryNo); + RootNode.Add(BuildOrderReference(OrderRefId)); + + // SellerSupplierParty + RootNode.Add(BuildParty('SellerSupplierParty', SellerName)); + + // BuyerCustomerParty + RootNode.Add(BuildParty('BuyerCustomerParty', BuyerName)); + end; + + local procedure BuildOrderReference(OrderId: Text) Node: XmlElement + var + IdNode: XmlElement; + begin + Node := XmlElement.Create('OrderReference', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); + IdNode := XmlElement.Create('ID', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', OrderId); + Node.Add(IdNode); + end; + + local procedure BuildParty(PartyRoleName: Text; PartyName: Text) RoleNode: XmlElement + var + PartyNode: XmlElement; + PartyNameNode: XmlElement; + NameNode: XmlElement; + begin + RoleNode := XmlElement.Create(PartyRoleName, 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); + + PartyNode := XmlElement.Create('Party', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); + PartyNameNode := XmlElement.Create('PartyName', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); + NameNode := XmlElement.Create('Name', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2', PartyName); + + PartyNameNode.Add(NameNode); + PartyNode.Add(PartyNameNode); + RoleNode.Add(PartyNode); + end; +}