From a4d8b77b59bc0ce6ed63087352e5420443052e8f Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi Date: Wed, 22 Apr 2026 04:09:26 +0800 Subject: [PATCH 1/4] fix(E-Document): loop all records in OnAfterSend and OnAfterSendToEMail subscribers When multiple posted documents are selected and sent using a Document Sending Profile configured for e-documents, only the first document was being created as an e-document. The root cause was that OnAfterSendEDocument and OnAfterSendToEMailEDocument each processed the incoming RecordVariant as a single record instead of iterating through all records. Use RecordRef.GetTable + FindSet/Next to iterate over every record in the variant. Pass the RecordRef for each iteration to both IsEDocumentCreatedForRecord and CreateEDocumentFromPostedDocument so each record is independently checked and created. Fixes #5650 --- .../EDocumentSubscribers.Codeunit.al | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) 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 b53bb12b12..e7342819ed 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -533,14 +533,19 @@ codeunit 6103 "E-Document Subscribers" local procedure OnAfterSendEDocument(ReportUsage: Integer; RecordVariant: Variant; DocNo: Code[20]; ToCust: Code[20]; DocName: Text[150]; CustomerFieldNo: Integer; DocumentNoFieldNo: Integer; DocumentSendingProfile: Record "Document Sending Profile") var EDocument: Record "E-Document"; + RecordRef: RecordRef; begin if DocumentSendingProfile."Electronic Document" <> Enum::"Doc. Sending Profile Elec.Doc."::"Extended E-Document Service Flow" then exit; if DocumentSendingProfile."Electronic Service Flow" = '' then exit; - if not EDocument.IsEDocumentCreatedForRecord(RecordVariant) then - CreateEDocumentFromPostedDocument(RecordVariant, DocumentSendingProfile); + RecordRef.GetTable(RecordVariant); + if RecordRef.FindSet() then + repeat + if not EDocument.IsEDocumentCreatedForRecord(RecordRef) then + CreateEDocumentFromPostedDocument(RecordRef, DocumentSendingProfile); + until RecordRef.Next() = 0; end; /// @@ -551,6 +556,7 @@ codeunit 6103 "E-Document Subscribers" local procedure OnAfterSendToEMailEDocument(var DocumentSendingProfile: Record "Document Sending Profile"; ReportUsage: Enum "Report Selection Usage"; RecordVariant: Variant; DocNo: Code[20]; DocName: Text[150]; ToCust: Code[20]; DocNoFieldNo: Integer; ShowDialog: Boolean) var EDocument: Record "E-Document"; + RecordRef: RecordRef; begin if DocumentSendingProfile."E-Mail" = DocumentSendingProfile."E-Mail"::"No" then exit; @@ -559,8 +565,12 @@ codeunit 6103 "E-Document Subscribers" Enum::"Document Sending Profile Attachment Type"::"PDF & E-Document"]) then exit; - if not EDocument.IsEDocumentCreatedForRecord(RecordVariant) then - CreateEDocumentFromPostedDocument(RecordVariant, DocumentSendingProfile); + RecordRef.GetTable(RecordVariant); + if RecordRef.FindSet() then + repeat + if not EDocument.IsEDocumentCreatedForRecord(RecordRef) then + CreateEDocumentFromPostedDocument(RecordRef, DocumentSendingProfile); + until RecordRef.Next() = 0; EDocumentProcessing.ProcessEDocumentAsEmail(DocumentSendingProfile, ReportUsage, RecordVariant, DocNo, DocName, ToCust, ShowDialog); end; From c234a590e83ded566d1b27e7a00c5d3c2a39522a Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi Date: Wed, 22 Apr 2026 17:44:34 +0800 Subject: [PATCH 2/4] test(E-Document): add AL test coverage for multi-record OnAfterSend fix Add EDocSendSubscriberTest codeunit (ID 139897) with two test procedures that exercise the RecordRef.FindSet/Next loop added in the fix: - SendMultiplePostedInvoicesCreatesEDocumentForEach: posts two invoices without auto E-Document creation, then sends both together via DocumentSendingProfile.Send() and asserts EDocument.Count() = 2. - SendSinglePostedInvoiceCreatesOneEDocument: regression guard ensuring a single-invoice send still produces exactly one E-Document. Pattern follows EDocEmailTests: disable DSP before posting, re-enable before sending, bind EDocImplState subscriber so the mock format produces non-empty TempBlob content through the processing pipeline. Closes #5650 --- .../EDocSendSubscriberTest.Codeunit.al | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al new file mode 100644 index 0000000000..a8ef17eb32 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al @@ -0,0 +1,134 @@ +// ------------------------------------------------------------------------------------------------ +// 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.Foundation.Reporting; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; +using Microsoft.Sales.History; +using System.TestLibraries.Utilities; + +codeunit 139897 "E-Doc. Send Subscriber Test" +{ + Subtype = Test; + TestPermissions = Disabled; + TestType = IntegrationTest; + Access = Internal; + + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + Assert: Codeunit Assert; + LibraryEDoc: Codeunit "Library - E-Document"; + LibrarySales: Codeunit "Library - Sales"; + EDocImplState: Codeunit "E-Doc. Impl. State"; + + [Test] + procedure SendMultiplePostedInvoicesCreatesEDocumentForEach() + var + SalesHeader: Record "Sales Header"; + SalesInvHeader1: Record "Sales Invoice Header"; + SalesInvHeader2: Record "Sales Invoice Header"; + SalesInvHeaderFilter: Record "Sales Invoice Header"; + DocumentSendingProfile: Record "Document Sending Profile"; + EDocument: Record "E-Document"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + // [FEATURE] [E-Document] [Processing] [Send] + // [SCENARIO] Sending multiple posted invoices together via Document Sending Profile creates one E-Document per invoice. + EDocument.DeleteAll(); + EDocumentServiceStatus.DeleteAll(); + EDocumentService.DeleteAll(); + DocumentSendingProfile.DeleteAll(); + BindSubscription(EDocImplState); + + LibraryEDoc.SetupStandardVAT(); + LibraryEDoc.SetupStandardSalesScenario(Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock); + + // [GIVEN] Document sending profile is temporarily disabled so posting does not auto-create E-Documents + DocumentSendingProfile.FindLast(); + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::No; + DocumentSendingProfile.Modify(); + + // [GIVEN] Two sales invoices are posted without automatic E-Document creation + LibraryEDoc.CreateSalesHeaderWithItem(Customer, SalesHeader, Enum::"Sales Document Type"::Invoice); + SalesInvHeader1.Get(LibrarySales.PostSalesDocument(SalesHeader, false, true)); + LibraryEDoc.CreateSalesHeaderWithItem(Customer, SalesHeader, Enum::"Sales Document Type"::Invoice); + SalesInvHeader2.Get(LibrarySales.PostSalesDocument(SalesHeader, false, true)); + + // [GIVEN] Document sending profile is re-enabled with the extended E-Document service flow + DocumentSendingProfile.FindLast(); + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::"Extended E-Document Service Flow"; + DocumentSendingProfile.Modify(); + + // [WHEN] Both posted invoices are sent together, simulating a multi-record send from the Posted Sales Invoices list + SalesInvHeaderFilter.SetFilter("No.", '%1|%2', SalesInvHeader1."No.", SalesInvHeader2."No."); + SalesInvHeaderFilter.FindFirst(); + DocumentSendingProfile.Send( + Enum::"Report Selection Usage"::"S.Invoice".AsInteger(), + SalesInvHeaderFilter, + SalesInvHeader1."No.", + SalesInvHeader1."Bill-to Customer No.", + 'Sales Invoice', + SalesInvHeaderFilter.FieldNo("Bill-to Customer No."), + SalesInvHeaderFilter.FieldNo("No.")); + + // [THEN] One E-Document is created for each of the two posted invoices + Assert.AreEqual(2, EDocument.Count(), 'Expected one E-Document per sent invoice.'); + UnbindSubscription(EDocImplState); + end; + + [Test] + procedure SendSinglePostedInvoiceCreatesOneEDocument() + var + SalesHeader: Record "Sales Header"; + SalesInvHeader: Record "Sales Invoice Header"; + DocumentSendingProfile: Record "Document Sending Profile"; + EDocument: Record "E-Document"; + EDocumentServiceStatus: Record "E-Document Service Status"; + begin + // [FEATURE] [E-Document] [Processing] [Send] + // [SCENARIO] Sending a single posted invoice via Document Sending Profile creates exactly one E-Document. + EDocument.DeleteAll(); + EDocumentServiceStatus.DeleteAll(); + EDocumentService.DeleteAll(); + DocumentSendingProfile.DeleteAll(); + BindSubscription(EDocImplState); + + LibraryEDoc.SetupStandardVAT(); + LibraryEDoc.SetupStandardSalesScenario(Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::Mock); + + // [GIVEN] Document sending profile is temporarily disabled so posting does not auto-create an E-Document + DocumentSendingProfile.FindLast(); + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::No; + DocumentSendingProfile.Modify(); + + // [GIVEN] A single sales invoice is posted without automatic E-Document creation + LibraryEDoc.CreateSalesHeaderWithItem(Customer, SalesHeader, Enum::"Sales Document Type"::Invoice); + SalesInvHeader.Get(LibrarySales.PostSalesDocument(SalesHeader, false, true)); + + // [GIVEN] Document sending profile is re-enabled with the extended E-Document service flow + DocumentSendingProfile.FindLast(); + DocumentSendingProfile."Electronic Document" := DocumentSendingProfile."Electronic Document"::"Extended E-Document Service Flow"; + DocumentSendingProfile.Modify(); + + // [WHEN] The single posted invoice is sent + SalesInvHeader.SetRecFilter(); + DocumentSendingProfile.Send( + Enum::"Report Selection Usage"::"S.Invoice".AsInteger(), + SalesInvHeader, + SalesInvHeader."No.", + SalesInvHeader."Bill-to Customer No.", + 'Sales Invoice', + SalesInvHeader.FieldNo("Bill-to Customer No."), + SalesInvHeader.FieldNo("No.")); + + // [THEN] Exactly one E-Document is created for the invoice + Assert.AreEqual(1, EDocument.Count(), 'Expected exactly one E-Document for a single sent invoice.'); + UnbindSubscription(EDocImplState); + end; +} From 88ceae2530876a7b5d289b9ede5502963830038a Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi Date: Thu, 23 Apr 2026 07:27:09 +0800 Subject: [PATCH 3/4] fix(E-Document): remove unused System.TestLibraries.Utilities using directive AL0792: The using directive was flagged as unused by the BC29 compiler. Assert is resolved without it in this compilation context. --- .../Test/src/Processing/EDocSendSubscriberTest.Codeunit.al | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al index a8ef17eb32..f54498419d 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al @@ -10,7 +10,6 @@ using Microsoft.Foundation.Reporting; using Microsoft.Sales.Customer; using Microsoft.Sales.Document; using Microsoft.Sales.History; -using System.TestLibraries.Utilities; codeunit 139897 "E-Doc. Send Subscriber Test" { From bc39a42e4396ca2dd3a7cafabba36bdf5c7c8f4b Mon Sep 17 00:00:00 2001 From: Jeffrey Bulanadi Date: Thu, 23 Apr 2026 21:37:01 +0800 Subject: [PATCH 4/4] fix(5650): use TypeHelper.CopyRecVariantToRecRef to handle Record and RecordRef variants Replace RecordRef.GetTable(RecordVariant) with the TypeHelper pattern already used in EDocumentProcessing.Codeunit.al. GetTable only accepts a Record variant and throws a runtime error when the caller passes a RecordRef variant. TypeHelper.CopyRecVariantToRecRef handles both IsRecord() and IsRecordRef() cases, preventing regressions when downstream callers pass a RecordRef. Add a guard clause that exits early if the variant is neither type, consistent with the existing pattern in EDocumentProcessing.GetTypeFromSourceDocument. --- .../src/Processing/EDocumentSubscribers.Codeunit.al | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 e7342819ed..c14787a98b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -533,6 +533,7 @@ codeunit 6103 "E-Document Subscribers" local procedure OnAfterSendEDocument(ReportUsage: Integer; RecordVariant: Variant; DocNo: Code[20]; ToCust: Code[20]; DocName: Text[150]; CustomerFieldNo: Integer; DocumentNoFieldNo: Integer; DocumentSendingProfile: Record "Document Sending Profile") var EDocument: Record "E-Document"; + TypeHelper: Codeunit "Type Helper"; RecordRef: RecordRef; begin if DocumentSendingProfile."Electronic Document" <> Enum::"Doc. Sending Profile Elec.Doc."::"Extended E-Document Service Flow" then @@ -540,7 +541,9 @@ codeunit 6103 "E-Document Subscribers" if DocumentSendingProfile."Electronic Service Flow" = '' then exit; - RecordRef.GetTable(RecordVariant); + if not (RecordVariant.IsRecord() or RecordVariant.IsRecordRef()) then + exit; + TypeHelper.CopyRecVariantToRecRef(RecordVariant, RecordRef); if RecordRef.FindSet() then repeat if not EDocument.IsEDocumentCreatedForRecord(RecordRef) then @@ -556,6 +559,7 @@ codeunit 6103 "E-Document Subscribers" local procedure OnAfterSendToEMailEDocument(var DocumentSendingProfile: Record "Document Sending Profile"; ReportUsage: Enum "Report Selection Usage"; RecordVariant: Variant; DocNo: Code[20]; DocName: Text[150]; ToCust: Code[20]; DocNoFieldNo: Integer; ShowDialog: Boolean) var EDocument: Record "E-Document"; + TypeHelper: Codeunit "Type Helper"; RecordRef: RecordRef; begin if DocumentSendingProfile."E-Mail" = DocumentSendingProfile."E-Mail"::"No" then @@ -565,7 +569,9 @@ codeunit 6103 "E-Document Subscribers" Enum::"Document Sending Profile Attachment Type"::"PDF & E-Document"]) then exit; - RecordRef.GetTable(RecordVariant); + if not (RecordVariant.IsRecord() or RecordVariant.IsRecordRef()) then + exit; + TypeHelper.CopyRecVariantToRecRef(RecordVariant, RecordRef); if RecordRef.FindSet() then repeat if not EDocument.IsEDocumentCreatedForRecord(RecordRef) then