E-Document: process all records in OnAfterSend and OnAfterSendToEMail#7785
Conversation
…il 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 microsoft#5650
|
This change needs to have tests that cover the scenario. |
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 microsoft#5650
|
Thank you for the feedback! Both test scenarios are already included in the second commit of this PR ( New test file: Test 1 - Multi-document scenario (the bug case):
Test 2 - Single-document regression guard:
Both tests follow the pattern from |
…irective AL0792: The using directive was flagged as unused by the BC29 compiler. Assert is resolved without it in this compilation context.
707bb4a
707bb4a to
88ceae2
Compare
There was a problem hiding this comment.
Pull request overview
Fixes E-Document creation when sending multiple posted documents via a Document Sending Profile by iterating over all records in the RecordVariant set, and adds regression tests to validate multi-record vs single-record sending.
Changes:
- Iterate through the full
RecordVariantset inOnAfterSendandOnAfterSendToEMailsubscribers to create an E-Document per selected posted document. - Add integration tests that verify E-Documents are created for both multi-invoice and single-invoice send scenarios.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al | Updates event subscribers to loop over all records in the passed RecordVariant before checking/creating E-Documents. |
| src/Apps/W1/EDocument/Test/src/Processing/EDocSendSubscriberTest.Codeunit.al | Adds new integration tests covering multi-record and single-record send behavior for posted sales invoices. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
… 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.
|
Re-running CI. |
|
@copilot pull main into branch |
…aftersend-loop-all-records
|
|
||
| // [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); |
There was a problem hiding this comment.
UnbindSubscription missing from try/finally
Both test procedures call UnbindSubscription(EDocImplState) at the end of the method body, but not inside a try/finally block. If any assertion or runtime error is thrown before reaching line 81 (or 131), the subscription remains bound for the remainder of the test session, potentially causing cascading failures in unrelated test codeunits.
Recommendation:
- Wrap the test body (after
BindSubscription) in a try/finally block and callUnbindSubscription(EDocImplState)in the finally branch to guarantee cleanup regardless of test outcome.
| UnbindSubscription(EDocImplState); | |
| BindSubscription(EDocImplState); | |
| try | |
| // ... test body ... | |
| finally | |
| UnbindSubscription(EDocImplState); | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
|
|
||
| // [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); |
There was a problem hiding this comment.
UnbindSubscription missing from try/finally
Same subscription-leak risk as the first test: UnbindSubscription(EDocImplState) is called at the end of SendSinglePostedInvoiceCreatesOneEDocument but not inside a finally block, leaving the subscription active if the test errors out mid-run.
Recommendation:
- Wrap the test body in a try/finally block and place
UnbindSubscriptionin the finally branch.
| UnbindSubscription(EDocImplState); | |
| BindSubscription(EDocImplState); | |
| try | |
| // ... test body ... | |
| finally | |
| UnbindSubscription(EDocImplState); | |
| end; |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| EDocument.DeleteAll(); | ||
| EDocumentServiceStatus.DeleteAll(); | ||
| EDocumentService.DeleteAll(); | ||
| DocumentSendingProfile.DeleteAll(); |
There was a problem hiding this comment.
DeleteAll on DocumentSendingProfile pollutes shared state
DocumentSendingProfile.DeleteAll() removes every document sending profile in the database. If another test codeunit in the same session runs after this and expects a default sending profile to already exist, it will fail with unexpected results. The cleanup at the start of each test covers the E-Document tables, but DocumentSendingProfile is shared system data.
Recommendation:
- Instead of
DeleteAll(), isolate the test by creating a new profile with a distinct code, use it explicitly, and delete only that record in cleanup (or inside a finally block).
| DocumentSendingProfile.DeleteAll(); | |
| // Create a dedicated test profile instead of DeleteAll | |
| DocumentSendingProfile.Init(); | |
| DocumentSendingProfile.Code := 'TEST-EDOC'; | |
| DocumentSendingProfile.Insert(); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
| EDocument.DeleteAll(); | ||
| EDocumentServiceStatus.DeleteAll(); | ||
| EDocumentService.DeleteAll(); | ||
| DocumentSendingProfile.DeleteAll(); |
There was a problem hiding this comment.
DeleteAll on DocumentSendingProfile pollutes shared state
Same broad DocumentSendingProfile.DeleteAll() in SendSinglePostedInvoiceCreatesOneEDocument carries the same cross-test contamination risk described in the first test.
Recommendation:
- Scope the document sending profile to a test-specific code and clean up only that record.
| DocumentSendingProfile.DeleteAll(); | |
| // Create a dedicated test profile instead of DeleteAll | |
| DocumentSendingProfile.Init(); | |
| DocumentSendingProfile.Code := 'TEST-EDOC'; | |
| DocumentSendingProfile.Insert(); |
👍 useful · ❤️ especially valuable · 👎 wrong - reply with why
Summary
When multiple posted documents are selected and sent using a Document Sending Profile configured for e-documents, only the first document was created as an e-document.
Root Cause
In EDocumentSubscribers.Codeunit.al, both OnAfterSendEDocument and OnAfterSendToEMailEDocument processed the incoming RecordVariant as a single record without iterating through all records in the set. The check and creation calls operated on the same fixed first record on every call.
Fix
Use RecordRef.GetTable + FindSet/Next to iterate over every record in the variant. In each iteration, the current RecordRef position is passed to IsEDocumentCreatedForRecord and CreateEDocumentFromPostedDocument so each document is independently checked and created.
For OnAfterSendToEMailEDocument, only the e-document creation loop is changed. The subsequent ProcessEDocumentAsEmail call continues to receive the original RecordVariant as it handles the email attachment step for the full selection.
Files Changed
Fixes #5650
Fixes AB#632028