Skip to main content

PO Email Notification

Extension ID: 6E56CA7D-E6F5-4AAC-8F2E-BEA9F0E5D49A Publisher: Sataware Technologies Current Version: 1.2.0.0 BC Compatibility: Platform 25.0+ / Application 25.0+


Purpose

Sends automated email notifications to the Supply Chain team at key milestones in the Purchase Order lifecycle:

TriggerEmail SentSubject
PO released"PO Created" — itemized list of linesPO {No.} Created
Warehouse Receipt created"PO Shipped" — lines on the receiptPO {No.} Shipped
Warehouse Receipt posted (or PO received directly)"PO Arrived" — posted receipt linesPO {No.} Arrived

Recipients are configured per-PO using a semicolon-separated list in the Send E-mail To field on the Purchase Order header. Email addresses are looked up from Entra ID via the Microsoft Graph API.


Architecture

Objects

ObjectIDTypePurpose
AzureAdMailLookup80100CodeunitAuthenticates with Microsoft Graph API via OAuth2 client credentials and queries Entra ID users by display name or email.
Temp Email Results80100Table (Temporary)In-memory result buffer for Graph API user search results. Never written to the database.
PO Email Setup80101TableSingleton setup record storing the Graph API tenant ID, client ID, and a flag indicating whether a client secret has been saved.
Purch. Order Ext80101Page ExtensionAdds the Send E-mail To field and email address lookup action to the Purchase Order page.
PO Email Setup80101PageAdministration page for configuring Graph API credentials. Accessible via Search → PO Email Setup.
PO Email Helper80103CodeunitBuilds email HTML and manages the enqueue lifecycle. All email sending goes through Email.Enqueue (non-blocking) so that email failures never roll back a posting transaction.
PO Email Subscribers80104CodeunitEvent subscribers that connect BC document lifecycle events to the email helper.
PoEmailPermissionSet80100Permission SetRequired permission set for users who need access to the setup page and email lookup.

Email Delivery

All emails are sent via Email.Enqueue, not Email.Send. This means:

  • Email delivery failures do not block or roll back Purchase Order or Warehouse Receipt operations.
  • Failed enqueue attempts are logged to Application Insights with event IDs ending in .fail (e.g., wr.shipped.enqueue.fail).
  • An administrator monitoring App Insights can detect delivery failures without user-facing disruption.

The BC Email Scenario "Custom Purchase Emails" must be mapped to an active email account. If the scenario has no mapped account, enqueues will fail silently in production — monitor App Insights for .enqueue.fail events.


Setup

PO Email Setup Flow

Prerequisites

  1. An Azure AD (Entra ID) app registration with:
    • API permission: User.Read.All (Application type)
    • A client secret configured
  2. The extension published to the BC environment
  3. The "Custom Purchase Emails" email scenario mapped to an active email account in BC (Settings → Email → Email Scenarios)
  4. The PoEmailPermissionSet permission set assigned to all users who will use the email lookup or access the setup page

Step 1: Configure Graph API Credentials

  1. Navigate to Search → PO Email Setup in BC.
  2. Enter the Graph Tenant ID (your Azure AD tenant ID, e.g., 823af2fd-...).
  3. Enter the Graph Client ID (the Application (client) ID from the app registration).
  4. Enter the Client Secret in the masked field.
  5. Click Save Secret to persist the secret to Isolated Storage.
    • The secret is stored in BC's Isolated Storage (DataScope::Module) and is never returned to the UI after saving.
    • The Client Secret Stored indicator will show true once saved.
  6. To rotate a secret: enter the new value and click Save Secret again. The old value is overwritten.
  7. To remove a secret: click Clear Secret.

Step 2: Configure Email Scenario

  1. Go to Settings → Email → Email Scenarios.
  2. Locate Custom Purchase Emails and assign it to the outbound email account that should send PO notifications.

Step 3: Set Recipients on a Purchase Order

  1. Open a Purchase Order.
  2. In the Send E-mail To field (in the General tab), enter one or more email addresses separated by semicolons (;).
  3. Use the Lookup action to search Entra ID users by name or email and select from the results. Multiple selections append to the field with semicolons.

How Notifications Work

PO Email Notification Flow

PO Created (on Release)

Fires when a Purchase Order is released (OnAfterReleasePurchaseDoc). The email lists all item lines on the PO at the time of release. No email is sent if "Send E-mail To" is blank.

PO Shipped (on WR Created)

Fires when a Warehouse Receipt is registered (OnAfterWhseRcptHeaderInserted). The email lists only the item lines on the warehouse receipt that are sourced from the PO. No email is sent if the PO has no recipients or if no matching WR lines are found.

PO Arrived (on Receipt Posted)

Fires on OnAfterProcessPurchLines in the purchase posting codeunit (Codeunit 90). This subscriber fires on any posting that produces a Purch. Rcpt. Header, including:

  • Warehouse Receipt posting
  • Direct PO posting with the Receive option

This is intentional — the "Arrived" notification applies to all receiving scenarios, not only the warehouse flow. Invoice-only postings (which produce no Purch. Rcpt. Header) are filtered out automatically.


Telemetry

All significant events are logged to Application Insights using the extension-level connection string in app.json.

Telemetry Event Map

Legend: Green = Normal | Amber = Warning | Red = Error

Event IDWhenSeverityKey Dimensions
setup.missingPO Email Setup record not found — extension unconfiguredError
setup.clientid.missingGraph Client ID is blank in setupWarning
setup.tenantid.missingGraph Tenant ID is blank in setupWarning
setup.secret.missingClient secret not saved in setupWarning
graph.token.connfailCannot reach Microsoft identity platformErrorTenant
graph.token.httpfailToken request returned HTTP errorErrorStatusCode
graph.search.connfailCannot reach Graph APIErrorFilter
graph.search.httpfailGraph API returned HTTP errorErrorStatusCode, DurationMs
graph.search.okEmail search completedNormalCount, DurationMs
graph.search.noresultsSearch returned zero usersWarningFilter
wr.shipped.startShipped notification startedNormalWR, PO
wr.shipped.eta.okETA resolvedNormal
wr.shipped.eta.missNo ETA foundWarning
wr.shipped.norecipNo recipients on POWarningPO
wr.shipped.lines.missNo matching WR linesWarning
wr.shipped.sentShipped email enqueuedNormalWR, PO, RecipientCount
wr.shipped.enqueue.failFailed to enqueue Shipped emailErrorWR, PO
arrived.startArrived notification startedNormalPO
arrived.countReceipt line count loggedNormalPostedWR, PO
arrived.norecipNo recipients resolvedWarningPO
arrived.nolinesPosted receipt has no item linesWarningPostedWR, PO
arrived.sentArrived email enqueuedNormalPostedWR, PO, RecipientCount
arrived.enqueue.failFailed to enqueue Arrived emailErrorPO
po.created.startPO Created notification startedNormalPO
po.created.norecipNo recipients on POWarningPO
po.created.nolinesPO has no item linesWarningPO
po.created.sentPO Created email enqueuedNormalPO, RecipientCount, LineCount
po.created.enqueue.failFailed to enqueue PO Created emailErrorPO

Query for recent enqueue failures:

traces
| where timestamp > ago(7d)
| where message has 'enqueue.fail'
| project timestamp, message
| order by timestamp desc

Query for setup misconfiguration events (typically the first thing to check after deployment):

traces
| where timestamp > ago(7d)
| where customDimensions.eventId in ('setup.missing', 'setup.clientid.missing', 'setup.tenantid.missing', 'setup.secret.missing')
| project timestamp, customDimensions.eventId, message
| order by timestamp desc

Query for Graph API latency (flag searches taking longer than 5 seconds):

traces
| where timestamp > ago(7d)
| where customDimensions.eventId == 'graph.search.ok'
| extend DurationMs = toint(customDimensions.DurationMs)
| where DurationMs > 5000
| project timestamp, message, DurationMs
| order by DurationMs desc

Automated Tests

23 Python-based integration tests in test/, organized across two files:

  • test_po_email.py (21 tests) — extension deployment, PO API lifecycle, App Insights telemetry, security verification, code quality checks, and app.json configuration validation
  • test_po_browser.py (2 tests) — Playwright browser tests that release POs through the BC web client UI, verifying the email subscriber fires end-to-end (with and without recipients)

Running Tests

cd "PO Email Notification/test"
python -m venv .venv && source .venv/bin/activate
pip install requests python-dotenv msal pytest pytest-json-report pytest-timeout

# BC API + code quality tests
python -m pytest -m bc -v

# Telemetry tests (requires az login)
python -m pytest -m telemetry -v

# Browser tests (requires Playwright + test user credentials)
pip install playwright pyotp && python -m playwright install chromium
python -m pytest -m browser -v

# All tests
python -m pytest -v

# Browser tests with visible browser
HEADED=1 python -m pytest -m browser -v

Environment Variables

Required in the project-root .env:

VariableDescription
BC_TENANT_IDAzure AD tenant ID
BC_CLIENT_IDApp registration client ID for BC API access
BC_CLIENT_SECRETClient secret for BC API access
BC_COMPANY_IDBC company ID (GUID)
BC_ENVIRONMENTBC environment name (defaults to Sandbox_Test_11062025)
BC_COMPANY_NAMEBC company name for browser deep links (defaults to Bestway Inc.)
TEST_VENDOR_NOVendor number for test PO creation (e.g., V00065)
TEST_ITEM_NOItem number for test PO lines (e.g., ZTEST-ITEM-001)

Additional variables for browser tests:

VariableDescription
BC_TEST_USER_UPNEntra user principal name for Playwright login
BC_TEST_USER_PASSWORDPassword for the test user
BC_TEST_USER_TOTP_SECRETBase32 TOTP secret for MFA (if the test user has MFA enabled)

Known Limitations

  • The PO Arrived subscriber fires on all purchase postings that create a receipt, including direct PO posting without a warehouse flow. This is by design — see the comment block in POEmailSubscribers.codeunit.al.
  • If the "Custom Purchase Emails" email scenario is not mapped to an account, enqueue failures are silent to end users and only visible in App Insights.
  • The recipient split uses ; as the delimiter. Comma-separated addresses in "Send E-mail To" will be treated as a single (invalid) address.
  • The Graph API user search applies $top=50. If more than 50 users match the search filter, only the first 50 are returned.