AI Automation

Explore
HomeEngineering BlogBuilding AppSource-Compliant Extensions for Dynamics 365 Business Central: A Deep Dive into Shipment Notes & Sales Control
DevOps & Engineering 12 min read DataDaur Microsoft Dynamics Engineering PracticePublished: 2026-08-04

Building AppSource-Compliant Extensions for Dynamics 365 Business Central: A Deep Dive into Shipment Notes & Sales Control

An enterprise engineering blueprint for developing cloud-ready, AppSource-compliant AL extensions for Microsoft Dynamics 365 Business Central v24+. Features AppSourceCop rules, state machine validation, event-driven sales posting interception, XLIFF localization, and automated AL unit testing.

AppSource-Compliant Extension Engineering in Microsoft Dynamics 365 Business Central
Click for Full Screen
Figure 1: Shipment Notes & Sales Control Extension Overview in Microsoft Dynamics 365 Business Central v24 CloudView GitHub Source Code →

By Data Daur Architecture Team

Published for Dynamics 365 Business Central Developers, Solution Architects, and Enterprise IT Leaders.


Executive Summary

In enterprise resource planning (ERP), logistics accuracy and sales posting safeguards are paramount. Unchecked posting routines on incomplete sales orders can trigger operational bottlenecks, premature inventory allocation, and compliance risks. Concurrently, logistics teams often require dedicated tracking mechanisms independent of standard posting workflows.

To address these core operational challenges, Data Daur developed Shipment Notes & Sales Control—a lightweight, enterprise-ready AL extension for Microsoft Dynamics 365 Business Central (Cloud, v24+).

This comprehensive technical guide explores how the extension was engineered from the ground up to achieve strict Microsoft AppSourceCop compliance, featuring state machine validation, event-driven posting interception, assignable security policies, translatable XLIFF localization, and automated unit testing.


Key Business Value & Features

CapabilityBusiness ImpactTechnical Implementation
Advanced Logistics TrackingIndependent shipment note lifecycle (Draft Pending Approved Released).Custom XYZ Shipment Note entity with table-level state transition rules.
Sales Order Quality ControlPrevents accidental posting of unverified sales orders.Event subscriber listening to Sales-Post.OnBeforePostSalesDoc.
Automated Document SeriesAuto-generates clean, sequential document numbers.Integrated with standard Codeunit "No. Series".
Enterprise ReadinessZero compiler warnings, 100% AppSource compliance.Strict affix matching (XYZ), assigned ID ranges (70000000+), and XLIFF localization.

1. AppSource Compliance & Metadata Configuration

Building cloud-ready extensions for Microsoft AppSource requires strict adherence to code analysis rules, object ID allocations, and naming conventions.

  • Assigned ID Range: 70000000 to 70000999
  • Mandatory Affix: XYZ (enforced via AppSourceCop.json)
  • Target Baseline: Cloud (BC 24.0+)
  • Localization Feature: Enabled TranslationFile in app.json for XLIFF translation generation.

AppSourceCop.json

AppSourceCop.json
{
  "mandatoryAffixes": [
    "XYZ"
  ]
}

app.json (Excerpt)

app.json
{
  "id": "5f87e36f-324b-410f-8acb-2568802267e5",
  "name": "Shipment Notes & Sales Control",
  "publisher": "Data Daur",
  "version": "1.0.1.0",
  "platform": "24.0.0.0",
  "application": "24.0.0.0",
  "target": "Cloud",
  "idRanges": [
    {
      "from": 70000000,
      "to": 70000999
    }
  ],
  "runtime": "17.0",
  "features": [
    "NoImplicitWith",
    "TranslationFile"
  ]
}

2. Feature Deep Dive: Advanced Shipment Notes & State Validation

Logistics teams require structured document lifecycles to prevent premature release of warehouse instructions. The extension introduces the XYZ Shipment Note table (70000001) paired with the XYZ Shipment Note Status Enum (70000000).

Status Enum Definition (XYZShipmentNoteStatus.Enum.al)

XYZShipmentNoteStatus.Enum.al
enum 70000000 "XYZ Shipment Note Status"
{
    Extensible = true;
    
    value(0; "Draft") { Caption = 'Draft'; }
    value(1; "Pending") { Caption = 'Pending'; }
    value(2; "Approved") { Caption = 'Approved'; }
    value(3; "Released") { Caption = 'Released'; }
}

State Transition Rule Enforcement (XYZShipmentNote.Table.al)

To prevent users from bypassing review stages (e.g., jumping directly from Draft to Released), validation logic is embedded directly within the field’s OnValidate() trigger:

XYZShipmentNote.Table.al
table 70000001 "XYZ Shipment Note"
{
    Caption = 'XYZ Shipment Note';
    DataClassification = CustomerContent;

    fields
    {
        field(1; "No."; Code[20]) { Caption = 'No.'; DataClassification = CustomerContent; }
        field(2; "Description"; Text[100]) { Caption = 'Description'; DataClassification = CustomerContent; }
        field(3; "Status"; Enum "XYZ Shipment Note Status")
        {
            Caption = 'Status';
            DataClassification = CustomerContent;

            trigger OnValidate()
            begin
                // Prevent invalid state transition from Draft directly to Released
                if (xRec.Status = Status::Draft) and (Status = Status::Released) then
                    Error(InvalidStatusTransitionErr, xRec.Status, Status);
            end;
        }
        field(4; "Posting Date"; Date) { Caption = 'Posting Date'; DataClassification = CustomerContent; }
        field(5; "Sales Header No."; Code[20])
        {
            Caption = 'Sales Header No.';
            DataClassification = CustomerContent;
            TableRelation = "Sales Header"."No." where("Document Type" = const(Order));
        }
        field(6; "No. Series"; Code[20])
        {
            Caption = 'No. Series';
            DataClassification = CustomerContent;
            TableRelation = "No. Series";
        }
    }

    trigger OnInsert()
    var
        XYZSetup: Record "XYZ Setup";
        NoSeries: Codeunit "No. Series";
    begin
        if "No." = '' then begin
            XYZSetup.Get();
            XYZSetup.TestField("XYZ Shipment Note Nos.");
            "No. Series" := XYZSetup."XYZ Shipment Note Nos.";
            "No." := NoSeries.GetNextNo("No. Series");
        end;
    end;

    var
        InvalidStatusTransitionErr: Label 'You cannot change the status directly from %1 to %2.', Comment = '%1 = Old Status, %2 = New Status';
}

3. Feature Deep Dive: Event-Driven Sales Posting Control

Instead of modifying standard Business Central source code, modern AL development relies on event subscribers.

The extension extends the standard Sales Header table and Sales Order page to introduce a manual XYZ Block Posting switch. When enabled, an event subscriber intercepts Sales-Post execution.

Table & Page Extensions

XYZSalesHeader.TableExt.al & XYZSalesOrder.PageExt.al
// TableExtension: XYZSalesHeader.TableExt.al
tableextension 70000000 "XYZ Sales Header Ext" extends "Sales Header"
{
    fields
    {
        field(70000000; "XYZ Block Posting"; Boolean)
        {
            Caption = 'XYZ Block Posting';
            DataClassification = CustomerContent;
        }
    }
}

// PageExtension: XYZSalesOrder.PageExt.al
pageextension 70000000 "XYZ Sales Order Ext" extends "Sales Order"
{
    layout
    {
        addlast(General)
        {
            field("XYZ Block Posting"; Rec."XYZ Block Posting")
            {
                ApplicationArea = All;
                ToolTip = 'Specifies if posting is blocked for this Sales Order.';
            }
        }
    }
}

Event Subscriber (XYZSalesPostingEventSub.Codeunit.al)

XYZSalesPostingEventSub.Codeunit.al
codeunit 70000000 "XYZ Sales Posting Event Sub"
{
    [EventSubscriber(ObjectType::Codeunit, Codeunit::"Sales-Post", 'OnBeforePostSalesDoc', '', false, false)]
    local procedure OnBeforePostSalesDoc(var SalesHeader: Record "Sales Header"; CommitIsSuppressed: Boolean; PreviewMode: Boolean)
    begin
        if SalesHeader."XYZ Block Posting" then
            Error(PostingBlockedErr, SalesHeader."No.");
    end;

    var
        PostingBlockedErr: Label 'Posting is blocked for Sales Order %1.', Comment = '%1 = Sales Order No.';
}

4. Enterprise Security & Translatable Localization

Explicit Permission Set (XYZPermissions.permissionset.al)

To adhere to modern Business Central security guidelines, an explicit Assignable = true permission set grants controlled RMID and execution rights across all custom objects:

XYZPermissions.permissionset.al
permissionset 70000000 XYZPermissions
{
    Assignable = true;
    Permissions = tabledata "XYZ Setup" = RMID,
        tabledata "XYZ Shipment Note" = RMID,
        table "XYZ Setup" = X,
        table "XYZ Shipment Note" = X,
        codeunit "XYZ Sales Posting Event Sub" = X,
        codeunit "XYZ Test Sales Block" = X,
        page "XYZ Setup Card" = X,
        page "XYZ Shipment Note List" = X,
        page "XYZ Shipment Note Card" = X;
}

Translatable Labels & XLIFF Localization

All hardcoded messages were converted into AL Label variables containing translator comments, ensuring clean auto-generation of XLIFF translation manifests (Shipment Notes & Sales Control.g.xlf).


5. Automated Unit Testing & Robust Assertions

Relying on empty asserterror blocks in automated test suites can lead to false positives (e.g., catching unrelated setup errors).

Data Daur built a dedicated assertion helper codeunit (XYZLibraryAssert.Codeunit.al) and an automated test suite (XYZTestSalesBlock.Codeunit.al).

XYZTestSalesBlock.Codeunit.al
codeunit 70000040 "XYZ Test Sales Block"
{
    Subtype = Test;

    [Test]
    procedure TestPostingBlockedWhenXYZBlockPostingIsTrue()
    var
        SalesHeader: Record "Sales Header";
        SalesPost: Codeunit "Sales-Post";
        LibraryAssert: Codeunit "Library Assert";
    begin
        // [Given] A Sales Order with XYZ Block Posting = TRUE
        Initialize();
        CreateSalesOrder(SalesHeader);
        SalesHeader."XYZ Block Posting" := true;
        SalesHeader.Modify();

        // [When] Posting is attempted
        // [Then] Verify that the error is caught using asserterror and matches explicitly
        asserterror SalesPost.Run(SalesHeader);
        
        LibraryAssert.ExpectedError(StrSubstNo(PostingBlockedErr, SalesHeader."No."));
    end;

    var
        PostingBlockedErr: Label 'Posting is blocked for Sales Order %1.', Comment = '%1 = Sales Order No.';
}

End-to-End Operational User Guide

Below is the step-by-step procedure for administrators and logistics coordinators to configure and utilize the extension in Business Central.

Step 1: Initial Setup & Number Series Assignment

  1. Open Business Central and press Alt + Q (Tell Me search).
  2. Type XYZ Setup and open the XYZ Setup Card.
  3. Under the General FastTab, locate XYZ Shipment Note Nos..
  4. Select or create a new Number Series (e.g., Code: XYZ-SHIP, Starting No.: SHP-0001, Ending No.: SHP-9999, Default Nos.: Yes).
  5. Assign XYZ-SHIP to the setup field and close the page.
XYZ Setup Card Configuration
Full Screen
Figure 2: Configuring XYZ Setup Card in Business Central
Creating Number Series XYZ-SHIP
Full Screen
Figure 3: Creating Sequential Number Series (XYZ-SHIP)

Step 2: Managing XYZ Shipment Notes

  1. Press Alt + Q and search for XYZ Shipment Notes.
  2. Click New to open the XYZ Shipment Note Card.
  3. The No. field automatically populates from your configured number series (SHP-0001).
  4. Select a related Sales Order No., enter a description, and select a posting date.
  5. State Transition Test: Attempt to change the Status from Draft directly to Released. The system will throw the translatable error:
    "You cannot change the status directly from Draft to Released."
  6. Transition sequentially through Draft Pending Approved Released.
XYZ Shipment Notes List View
Full Screen
Figure 4: XYZ Shipment Notes List View
XYZ Shipment Note Card State Transition Test
Full Screen
Figure 5: State Validation Interception on Shipment Note Card

Step 3: Enforcing & Testing Sales Order Posting Interception

  1. Navigate to Sales Orders and open an existing active order.
  2. Under the General FastTab, locate XYZ Block Posting and switch it to TRUE.
  3. Click Posting Post... (F9).
  4. Select Ship and Invoice and press OK.
  5. Result: The posting process is safely intercepted, presenting a clear error notification:
    "Posting is blocked for Sales Order 101005."
  6. Toggle XYZ Block Posting off (FALSE) to resume normal posting capabilities.
Sales Order Interception Error Verification
Full Screen
Figure 6: Posting Interception Error Notification on Sales Order 101005

Conclusion & Key Developer Takeaways

Building extensions for Dynamics 365 Business Central Cloud demands a discipline of strict governance:

  1. Always use AppSourceCop: Enforce mandatory affixes (XYZ) and clean ID allocations (70000000+).
  2. Never hack standard objects: Utilize clean event subscriptions (OnBeforePostSalesDoc) to extend standard behaviors safely.
  3. Enforce business logic at the table layer: Embedding state machine rules in OnValidate() ensures rules are honored regardless of which UI page or API triggers the edit.
  4. Assert explicit error messages in tests: Avoid generic asserterror statements; always verify GetLastErrorText() matches expected labels.

About Data Daur

Data Daur specializes in enterprise Dynamics 365 Business Central development, AppSource extension engineering, and cloud architecture solutions.

Need Expert Technical Guidance?

Consult directly with DataDaur's Senior ERP Architects & AI Engineers for custom architecture reviews, migration strategies, or technical audits.

Share Blog Post:

Editorial & Compliance Disclaimer: DataDaur is an independent enterprise software and AI consulting firm. Microsoft, Microsoft Dynamics 365, Business Central, Microsoft Power Platform, Dataverse, Copilot, Odoo, and related marks are registered trademarks of their respective owners. All technical specs, release data, and API capabilities documented in this blog post are cross-verified directly against official Microsoft Learn (learn.microsoft.com) and vendor documentation. System capabilities may vary depending on licensing tiers, regional localizations, and tenant configurations.

Book an Engineering Blog Consultation

Discuss custom architecture, AI agents, or ERP implementation strategy with our senior engineers.

Get In Touch

Let's Start a Conversation

Tell us about your project and we'll connect you with the right team.