Change Document — PO Email Notification v1.2.0.0
Date: March 20, 2026 Extension: PO - Email Notification Version: 1.2.0.0 (from 1.1.1.0) Publisher: Sataware Technologies
Background
An independent code review and performance review of v1.1.1.0 identified 8 issues requiring fixes before production deployment. The most significant are field ID and enum value collisions with the BestwayUSA extension's reserved ID range (50100–50149), a functional bug in the email address lookup that corrupts the recipient list, and incomplete SetLoadFields declarations that were only partially addressed in v1.1.1.0.
Production currently runs v1.0.88.5 — the original Sataware release with hardcoded credentials. No email telemetry has been recorded in the last 90 days; the extension's email functionality was never operational in production. The "Send E-mail To" field exists in the production schema but is confirmed empty across all Purchase Headers, Warehouse Receipt Headers, and Posted Warehouse Receipt Headers. This means the field ID migration (50119 to 80100) can be a direct swap with no data migration required.
Summary of Changes
| # | Severity | Category | Description |
|---|---|---|---|
| 1 | Critical | Correctness | Field ID 50119 migrated to 80100 on all three table extensions — eliminates collision with BestwayUSA's reserved range |
| 2 | Critical | Correctness | Enum value 50149 migrated to 80100 in Email Scenario extension — eliminates collision with BestwayUSA's reserved range |
| 3 | Critical | Correctness | Unguarded Get() calls wrapped with if not...then exit in two codeunits — prevents runtime errors that would block warehouse receipt creation |
| 4 | Critical | Bug fix | OnLookup concatenation bug fixed — search text was being embedded in the recipient list instead of appending to the existing field value |
| 5 | Low | Code quality | Permission set updated with Caption and Assignable = true — users previously saw raw name "PoEmailPermissionSet" in BC |
| 6 | Low | Correctness | Four SetLoadFields declarations completed with missing filter fields — same class of issue v1.1.1.0 partially addressed |
| 7 | Low | Performance | SetLoadFields added to PurchHeader.Get() inside subscriber loop — reduces 200+ field loads to 4 per iteration |
| 8 | Low | Dead code | PO Email Dispatcher codeunit (80102) removed — unreachable dead code with no callers anywhere in the codebase |
Detailed Changes
1. Field ID Migration: 50119 to 80100 (Critical — Correctness)
Files:
src/tableExtensions/PurchaseHeaderExt.tableExt.alsrc/tableExtensions/WhseRcptHdrExt.tableExt.alsrc/tableExtensions/PostedWhseRcptHdrExt.tableExt.al
Before: All three table extensions defined the "Send E-mail To" field as field(50119; ...). Field ID 50119 falls within BestwayUSA's reserved range (50100–50149), and BestwayUSA already uses 50119 on the Item table. If BestwayUSA ever adds field 50119 to Purchase Header, Warehouse Receipt Header, or Posted Whse. Receipt Header, this extension would fail to install.
After: Field ID changed to 80100 on all three table extensions — within this extension's own reserved range (80100–80149). Because the field is confirmed empty in both production and sandbox (zero email telemetry in 90 days, extension never configured), this is a schema-breaking change that requires uninstall/reinstall rather than an in-place upgrade. No data is lost.
2. Enum Value Migration: 50149 to 80100 (Critical — Correctness)
File: src/enumExtensions/EmailScenarioExt.enumExt.al
Before: The "Custom Purchase Emails" value was defined as value(50149; ...). Enum value 50149 is in BestwayUSA's reserved range — the same collision risk as the field IDs.
After: Changed to value(80100; ...). Since the extension's email functionality was never operational in production, no existing Email Scenario mappings reference this value. The uninstall/reinstall deployment path clears the old enum registration.
3. Guard Get() Calls (Critical — Correctness)
Files:
src/codeunits/EmailFieldTransferSubscribers.codeunit.alsrc/codeunits/POEmailDispatcher.codeunit.al(subsequently deleted — see change 8)
Before: Two Get() calls would throw unhandled runtime errors if the target Purchase Header record did not exist. In EmailFieldTransferSubscribers, this would block warehouse receipt creation entirely — the event subscriber fires during WR creation and a Get() failure would roll back the entire transaction.
In EmailFieldTransferSubscribers (line 12):
Purchaseheader.Get(Purchaseheader."Document Type"::Order, Rec."No.");
After: Both Get() calls wrapped with if not...then exit:
if not Purchaseheader.Get(Purchaseheader."Document Type"::Order, Rec."No.") then
exit;
On a failed Get(), the procedure exits silently — no error, no telemetry. The record no longer exists, so there is nothing to process. The guard in POEmailDispatcher was applied for correctness before the codeunit was subsequently deleted in change 8.
4. Fix OnLookup Concatenation Bug (Critical — Bug Fix)
File: src/pageExtensions/PurchOrderExt.pageExt.al — line 28 (the then branch)
Before: The OnLookup trigger on "Send E-mail To" appended the selected email to the Text parameter (the user's search input) instead of to Rec."Send E-mail To" (the existing field value). This embedded the search text permanently in the recipient list.
Example: Field contains bob@bestwaycorp.us. User types "ali" to search, selects alice@bestwaycorp.us. Result: ali;alice@bestwaycorp.us instead of the correct bob@bestwaycorp.us;alice@bestwaycorp.us.
// Before (line 28 — then branch only)
Text := Text + ';' + TempResults.Email
// After
Text := Rec."Send E-mail To" + ';' + TempResults.Email
The else branch (Text := TempResults.Email) is correct and was not changed — it handles the case where the field is empty and the user is selecting the first recipient.
5. Add Caption and Assignable to Permission Set (Low — Code Quality)
File: src/permissionset/PoEmailPermissionSet.permissionset.al
Before: The permission set had no Caption property, so users saw the raw internal name "PoEmailPermissionSet" in the BC permission set assignment UI. No explicit Assignable = true declaration.
After: Added Caption = 'PO Email Notification'; and Assignable = true;. Users now see a human-readable name when assigning the permission set.
6. Complete SetLoadFields Declarations — 4 Queries (Low — Correctness)
File: src/codeunits/POEmailHelper.codeunit.al
Four SetLoadFields calls were missing their corresponding filter fields. This is the same class of issue v1.1.1.0 was released to fix, but the fix was applied inconsistently — four queries were missed.
| Location | Query | Missing Fields Added |
|---|---|---|
Notify_POCreated_OnRelease PurchaseLine query | SetRange("Document Type", ...) / SetRange("Document No.", ...) | "Document Type", "Document No." |
Notify_Shipped WarehouseReceiptLine query | SetRange("No.", ...) | "No." |
GetPOETA PurchaseLine query | SetRange("Document Type", ...) / SetRange("Document No.", ...) | "Document Type", "Document No." |
BuildHtmlTableFromPostedWRLines_All PurchRcptLine query | SetRange("Document No.", ...) | "Document No." |
BC runtime 12.0+ auto-includes filter fields in the SQL projection, so these omissions did not cause runtime failures. However, omitting filter fields from SetLoadFields is misleading to developers reading the code and fragile across platform versions. This brings all SetLoadFields declarations in the extension to full correctness.
7. Add SetLoadFields to PurchHeader.Get() in Subscriber Loop (Low — Performance)
File: src/codeunits/POEmailSubscribers.codeunit.al
Before: PurchHeader.Get() inside the foreach PONo in UniquePOs loop loaded all 200+ fields from the Purchase Header table, but only 4 fields are consumed downstream: "No.", "Document Type", "Send E-mail To", and "Expected Receipt Date".
After: PurchHeader.SetLoadFields("No.", "Document Type", "Send E-mail To", "Expected Receipt Date") added before the foreach loop. SetLoadFields is sticky on the record variable and applies to all subsequent Get() calls in the loop. This is safe because the PurchHeader variable is read-only throughout — it is passed to Notify_Shipped_OnWhseReceiptCreated, which reads "Send E-mail To" (via BuildRecipientListFromPO), "No.", and "Expected Receipt Date" (via GetPOETA, which accepts var but only reads). All four fields are in the load list.
Impact: For a warehouse receipt covering 5 POs, this reduces the data loaded from the Purchase Header table by roughly 98% per Get() call (4 fields instead of 200+).
8. Remove PO Email Dispatcher Dead Code (Low — Dead Code)
File deleted: src/codeunits/POEmailDispatcher.codeunit.al
File modified: src/permissionset/PoEmailPermissionSet.permissionset.al — removed codeunit "PO Email Dispatcher" = X entry
Codeunit 80102 (PO Email Dispatcher) had TableNo = "Purchase Header" and an OnRun trigger, but nothing in the codebase called it. The PO Created notification is handled by POEmailSubscribers.OnAfterReleasePurchaseDoc, and the manual action on the Purchase Order page calls Helper.Notify_POCreated_OnRelease directly. A full-text search of the repository confirmed no references to Codeunit.Run(80102, ...), TaskScheduler.CreateTask(Codeunit::"PO Email Dispatcher", ...), or any other invocation pattern.
The codeunit was removed entirely. Its ID (80102) is released back to the extension's available range.
Object Inventory
15 objects after deletion of the Dispatcher codeunit:
| Object | ID | Type | Status |
|---|---|---|---|
AzureAdMailLookup | 80100 | Codeunit | Unchanged |
EmailTransferSubscribers | 80101 | Codeunit | Modified — Get() guarded |
PO Email Dispatcher | 80102 | Codeunit | Deleted — dead code removed |
PO Email Helper | 80103 | Codeunit | Modified — 4x SetLoadFields completed |
PO Email Subscribers | 80104 | Codeunit | Modified — SetLoadFields added to loop |
Temp Email Results | 80100 | Table | Unchanged |
PO Email Setup | 80101 | Table | Unchanged |
Posted Whse Rcpt Hdr Ext | 80100 | Table Extension | Modified — field 50119 to 80100 |
PurchaseHeaderExt | 80101 | Table Extension | Modified — field 50119 to 80100 |
Whse Rcpt Hdr Ext | 80102 | Table Extension | Modified — field 50119 to 80100 |
Email Scenario Ext | 80100 | Enum Extension | Modified — value 50149 to 80100 |
Azure AD Mail Lookup Page | 80100 | Page | Unchanged |
PO Email Setup | 80101 | Page | Unchanged |
Posted Whse Rcpt Ext | 80100 | Page Extension | Unchanged |
Purch. Order Ext | 80101 | Page Extension | Modified — OnLookup bug fixed |
Whse Rcpt Ext | 80102 | Page Extension | Unchanged |
PoEmailPermissionSet | 80100 | Permission Set | Modified — Caption, Assignable, Dispatcher entry removed |
Note: The table lists 16 rows including the deleted Dispatcher. The extension ships with 15 active objects.
File Structure
PO Email Notification/
├── app.json (version 1.2.0.0)
├── README.md
├── docs/
│ ├── CHANGELOG.md
│ ├── CHANGE-v1.1.0.0.md
│ ├── CHANGE-v1.1.1.0.md
│ ├── CHANGE-v1.2.0.0.md (this file)
│ └── superpowers/
│ └── specs/
│ └── 2026-03-20-v1.2.0.0-id-migration-and-fixes.md
├── src/
│ ├── codeunits/
│ │ ├── AzureAdMailLookup.Codeunit.al
│ │ ├── EmailFieldTransferSubscribers.codeunit.al
│ │ ├── POEmailHelper.codeunit.al
│ │ └── POEmailSubscribers.codeunit.al
│ ├── enumExtensions/
│ │ └── EmailScenarioExt.enumExt.al
│ ├── pageExtensions/
│ │ ├── PostedWhseRcptExt.pageExt.al
│ │ ├── PurchOrderExt.pageExt.al
│ │ └── WhseRcptExt.pageExt.al
│ ├── pages/
│ │ ├── AzureADMailLookupPage.page.al
│ │ └── POEmailSetup.page.al
│ ├── permissionset/
│ │ └── PoEmailPermissionSet.permissionset.al
│ ├── tableExtensions/
│ │ ├── PostedWhseRcptHdrExt.tableExt.al
│ │ ├── PurchaseHeaderExt.tableExt.al
│ │ └── WhseRcptHdrExt.tableExt.al
│ └── tables/
│ ├── POEmailSetup.table.al
│ └── TempEmailResults.table.al
└── test/
├── conftest.py
├── pytest.ini
├── test_po_browser.py
└── test_po_email.py
Known Limitations
Carried forward from v1.1.0.0 — no changes:
- The recipient semicolon delimiter is fixed — comma-separated addresses are not supported.
- Graph API results are capped at 50 per search. If a name or email prefix matches more than 50 users, only the first 50 are returned.
- The Arrived notification fires on all purchase postings that create a receipt (including direct PO posting without a warehouse flow). This is by design and is documented in the subscriber code.
- Email failures are silent to end users. App Insights monitoring is required to detect enqueue failures.
Deployment Notes
Because the field IDs change (50119 to 80100) and the enum value changes (50149 to 80100), neither environment can do an in-place upgrade — the schema delta would drop the old field and create a new one. Since the field is confirmed empty in both environments, this is safe. Both sandbox and production follow the same deployment path: uninstall the current version, then install v1.2.0.0 fresh.
Pre-Deployment
- Confirm "Send E-mail To" is empty on Purchase Headers in the target environment (it should be — the extension was never configured in production).
- Note the Azure AD app registration credentials (tenant ID, client ID, client secret) — you will need them for post-deployment configuration.
Deployment Steps
- Uninstall the current version from the target environment via Extension Management:
- Production: v1.0.88.5
- Sandbox: v1.1.1.0
- Install v1.2.0.0 by uploading the
.appfile. - Open PO Email Setup and configure Graph API credentials (tenant ID, client ID, client secret). Click Save Secret to persist the client secret in Isolated Storage.
- Map the "Custom Purchase Emails" Email Scenario to an active outbound email account via Email Accounts > Email Scenarios.
- Assign the PO Email Notification permission set to all users who need access to the email lookup or setup page.
- Verify: Release a Purchase Order with "Send E-mail To" populated and confirm the email is received.
- Monitor: Check App Insights for
setup.missingorenqueue.failevents in the first 24 hours.
Rollback
If issues are found after deployment, uninstall v1.2.0.0. No data will be lost because the extension's custom fields will be empty (fresh install). The base BC Purchase Order functionality is unaffected.
Testing
The automated test suite has 23 tests (21 API/code-quality + 2 Playwright browser tests), all passing. The browser tests exercise the full PO Release flow through the BC web client, verifying that the email subscriber fires end-to-end — covering the gap that the API-based release test could not (Microsoft.NAV.releaseDocument bound action is not exposed on this sandbox's API).
Run the automated test suite to confirm no regressions:
cd "PO Email Notification/test"
source .venv/bin/activate
python -m pytest -v
Expected: 23 passed, 0 failed.
All 22 UAT test cases from the v1.1.0.0 test plan remain valid. No new UAT test cases are required — changes 1–3 and 5–8 are internal correctness and performance improvements with no user-observable behavior change, and change 4 (OnLookup fix) is covered by existing TC-2.2 (multi-recipient selection via lookup).
Not In Scope
The following items from the code review and performance review were evaluated and deferred or accepted:
| Item | Review Finding | Decision | Rationale |
|---|---|---|---|
| Email address validation | Code Review #6 | Deferred | BC's Email module provides downstream validation |
| Hardcoded footer contact email | Code Review #7 | Accepted | Low change frequency; not worth the abstraction |
| HTML row builder extraction | Code Review #8 | Accepted | Not worth the added abstraction for three similar but not identical templates |
| PII in telemetry | Code Review #18 | Accepted | Search text is partial strings, not full identifiers |
| File naming standardization | Code Review #13 | Deferred | Avoid unnecessary churn in this release |