Microsoft AL App Extensions

repository·main·Indexed 21 days ago

https://github.com/microsoft/alappextensions

A repository for requesting extensibility enhancements for Microsoft Dynamics 365 Business Central, such as new integration events, visibility changes, and enum updates. It also contains source code and documentation for the Contoso Coffee Demo Data Set, the AI Development Toolkit (including Design and Evaluation apps), and the Microsoft Universal Print extension.

Tokens
8.5K
Snippets
13
Records
30
Agent score
76%

What's inside alappextensions

  1. Overview of the Contoso Coffee Demo Data Set

    main
    The Contoso Coffee Demo Data Set provides fictional company data for Business Central, allowing users to learn and explore the platform's capabilities. Unlike the legacy rapidstart packages, the modern Demo Data App uses an extensibility-focused, modular approach. It allows for scenario-based demonstrations by selecting specific modules and their dependencies during company creation.
  2. Understand the Demo Data Layers

    main

    Data within a demo data module is organized into four distinct layers to allow for progressive demonstration depth:

    1. Setup: Foundational elements used in production companies, such as GL accounts and VAT posting.
    2. Master: Primary records used for demonstrations, such as customers and vendors.
    3. Transactional: Open documents, such as sales and purchase invoices.
    4. Historical: Posted documents used to facilitate reporting and analytics demonstrations.
  3. Manage module dependencies and execution order

    main

    The Contoso Demo Tool supports dependencies between modules, allowing one module to leverage data generated by another.

    Execution Order Pattern:

    1. Setup Data (e.g., Foundation)
    2. Setup Data (e.g., Finance)
    3. Master Data (e.g., Foundation)
    4. Master Data (e.g., Finance)

    Critical Requirement: When referencing data from a previous module, always use .Validate() instead of direct value assignment. This ensures that the referenced data is properly processed and prevents errors if the data does not yet exist.

  4. Implement GL Account localization patterns

    main

    GL Accounts follow a specific lifecycle to allow localization apps to modify account numbers. The process involves registering a base account for the W1 localization and then allowing subsequent localization apps to intercept and modify that account number via an integration event.

    The GL Account Lifecycle

    1. Initialization: Call AddGLAccountsForLocalization() at the start of your process. This procedure must add the base account for the W1 localization using ContosoGLAccount.AddAccountForLocalization(AccountName, AccountNo). This creates a key-value pair in the temporary table 4769 "Contoso GL Account" (e.g., 'Employees Payable' -> '5850').
    2. Interception: Immediately after adding the W1 account, fire the OnAfterAddGLAccountsForLocalization() integration event. This allows localization apps (e.g., a Canada localization app) to call AddAccountForLocalization again with the same AccountName but a different AccountNo (e.g., 'Employees Payable' -> '23850').
    3. Resolution: When the application needs the account number, call the descriptive procedure (e.g., EmployeesPayable()). This procedure should use ContosoGLAccount.GetAccountNo(AccountName) to look up the current localized value from the temporary table.

    Implementation Example

    To implement this pattern, define a descriptive procedure for the account number and a name procedure, and manage the registration in a setup procedure.

    // Example implementation of the GL Account pattern
    
    trigger OnRun()
    begin
        AddGLAccountsForLocalization();
    
        // Use the descriptive procedure to get the (potentially localized) account number
        ContosoGLAccount.InsertGLAccount(EmployeesPayable(), EmployeesPayableName(), ...);
    end;
    
    local procedure AddGLAccountsForLocalization()
    begin
        // 1. Add the base W1 account
        ContosoGLAccount.AddAccountForLocalization(EmployeesPayableName(), '5850');
    
        // 2. Fire event to allow localization apps to modify the AccountNo
        OnAfterAddGLAccountsForLocalization();
    end;
    
    var
        ContosoGLAccount: Codeunit "Contoso GL Account";
        EmployeesPayableLbl: Label 'Employees Payable', MaxLength = 100;
    
    // 3. The consumer calls this to get the localized AccountNo
    procedure EmployeesPayable(): Code[20]
    begin
        exit(ContosoGLAccount.GetAccountNo(EmployeesPayableName()));
    end;
    
    procedure EmployeesPayableName(): Text[100]
    begin
        exit(EmployeesPayableLbl);
    end;
    
    [IntegrationEvent(false, false)]
    local procedure OnAfterAddGLAccountsForLocalization()
    begin
    end;
  5. Use descriptive methods for reusable labels

    main

    To ensure data consistency and translation accuracy, avoid using raw label strings directly in business logic. Instead, define descriptive methods that return specific labels. This prevents typos across different tables and ensures that the same concept (e.g., 'Sneakers') is translated consistently across all documents.

    Pattern:

    1. Define Label variables with appropriate MaxLength.
    2. Create a procedure that returns the specific label token.
    procedure Sneakers(): Code[20]
    begin
        exit(SneakersTok);
    end;
    
    var
        SneakersTok: Label 'SNEAKERS', MaxLength = 20;
  6. Types of accepted extensibility requests

    main

    The repository accepts specific categories of extensibility requests to unblock app development. Use the following classifications when submitting your request:

    • Event-request: Requesting a new integration event.
    • Request-for-external: Requesting that a function be made external or otherwise callable from an extension.
    • Enum-request: Requesting that an option be replaced with an extensible enum.
    • Extensibility-enhancement: Requesting broader changes that improve overall extensibility.
    • Extensibility-bug: Requesting small fixes specifically to unblock an extensibility scenario.
  7. Extensibility and Customization features

    main

    The Contoso Demo Tool is designed for partners to customize and extend demo scenarios through several mechanisms:

    • Helpers Codeunit: Provides a dedicated codeunit to simplify the insertion and updating of records in commonly used tables.
    • Localization: Uses a localization extension approach where country-specific versions of Contoso are provided to localize data.
    • Extensibility Points: Offers multiple hooks before running the tool and between each data layer, allowing partners to customize data flow.
    • Translation: Uses a code-based approach to provide multiple translation apps, replacing the old file-based rapidstart method.
  8. Where to report issues and contributions for Business Central

    main

    This repository is strictly for extensibility requests. Do not use it for the following purposes:

    GoalCorrect Destination
    Code contributions and Pull RequestsBCApps
    Product ideasBC Ideas
    Product defects or customer-impacting issuesBusiness Central support

    Note: Issues filed here do not fall under SLAs, have no guaranteed mitigation time, and fixes are not guaranteed to be backported to all supported versions.

  9. Use Helper Codeunits for data insertion

    main

    Helper codeunits (found in DemoTool/Contoso Helpers/...) simplify inserting demo data by hiding complexity and handling cross-company logic (e.g., W1 vs NA).

    Key Features of Helpers:

    • Company Type Abstraction: Automatically handles Company Type logic.
    • Conflict Management: Use the Exists and OverwriteData variables to control behavior when a record already exists. If OverwriteData is true, the helper will update the existing record instead of throwing an error.
    // Example: Inserting a Product Posting Group using a helper
    procedure InsertGenProductPostingGroup(ProductGroupCode: Code[20]; Description: Text[100]; DefaultVATProdPostingGroup: Code[20])
    var
        ContosoCoffeeDemoDataSetup: Record "Contoso Coffee Demo Data Setup";
        GenProductPostingGroup: Record "Gen. Product Posting Group";
        Exists: Boolean;
    begin
        ContosoCoffeeDemoDataSetup.Get();
    
        if GenProductPostingGroup.Get(ProductGroupCode) then begin
            Exists := true;
            if not OverwriteData then
                exit;
        end;
    
        GenProductPostingGroup.Validate(Code, ProductGroupCode);
        GenProductPostingGroup.Validate(Description, Description);
    
        if ContosoCoffeeDemoDataSetup."Company Type" = ContosoCoffeeDemoDataSetup."Company Type"::VAT then
            GenProductPostingGroup.Validate("Def. VAT Prod. Posting Group", DefaultVATProdPostingGroup);
    
        if Exists then
            GenProductPostingGroup.Modify(true)
        else
            GenProductPostingGroup.Insert(true);
    end;
  10. Add Module Configuration (Optional)

    main

    To allow users to configure your module, implement a setup table and a configuration page. Ensure your data generation logic uses the values stored in these configuration tables.

    Setup Table Requirements:

    • Define fields for configuration (e.g., StartDate).
    • Use InherentPermissions for initialization logic.
    • Set DataPerCompany = true if configuration is company-specific.

    Setup Page Requirements:

    • Use PageType = Card.
    • Set SourceTable to your setup table.
    • Use OnOpenPage to trigger record initialization (e.g., Rec.InitRecord()).
    table 5282 "Module Setup"
    {
        DataClassification = CustomerContent;
        InherentEntitlements = RMX;
        InherentPermissions = RMX;
        Extensible = false;
        DataPerCompany = true;
        ReplicateData = false;
    
        fields
        {
            field(1; "Primary Key"; Integer) { DataClassification = SystemMetadata; }
            field(2; StartDate; Date) { Caption = 'Start Date'; }
        }
    
        keys
        {
            key(Key1; "Primary Key") { Clustered = true; }
        }
    
        [InherentPermissions(PermissionObjectType::TableData, Database::"Module Setup", 'I')]
        internal procedure InitRecord()
        begin
            if Rec.Get() then
                exit;
            Rec.Insert();
        end;
    }
    
    page 5281 "Module Setup"
    {
        PageType = Card;
        SourceTable = "Module Setup";
        
        layout
        {
            area(Content)
            {
                group("Setup Data")
                {
                    field(StartDate; Rec.StartDate) { }
                }
            }
        }
    
        trigger OnOpenPage()
        begin
            Rec.InitRecord();
        end;
    }
    table 5282 "Module Setup"
    {
        DataClassification = CustomerContent;
        InherentEntitlements = RMX;
        InherentPermissions = RMX;
        Extensible = false;
        DataPerCompany = true;
        ReplicateData = false;
    
        fields
        {
            field(1; "Primary Key"; Integer)
            {
                DataClassification = SystemMetadata;
                Caption = 'Primary Key';
            }
            field(2; StartDate; Date)
            {
                Caption = 'Start Date';
                ToolTip = 'Specifies the start date for the scenario.';
            }
        }
    
        keys
        {
            key(Key1; "Primary Key")
            {
                Clustered = true;
            }
        }
    
        [InherentPermissions(PermissionObjectType::TableData, Database::"Module Setup", 'I')]
        internal procedure InitRecord()
        begin
            if Rec.Get() then
                exit;
    
            Rec.Insert();
        end;
    }
    
    page 5281 "Module Setup"
    {
        PageType = Card;
        Caption = 'Module Setup';
        SourceTable = "Module Setup";
        Extensible = false;
        DeleteAllowed = false;
        InsertAllowed = false;
    
        layout
        {
            area(Content)
            {
                group("Setup Data")
                {
                    field(StartDate; Rec.StartDate) { }
                }
            }
        }
    
        trigger OnOpenPage()
        begin
            Rec.InitRecord();
        end;
    }