Monday, 9 March 2026

Create Purchase Orders from Drop Shipments

If you have worked with drop shipments in Business Central, you know the process has always been a bit... indirect. You would create a sales order, mark lines for drop shipment, and then separately create a purchase order and use the Get Sales Orders action to pull those lines in. It worked, but it was not exactly smooth, especially when you were juggling multiple orders.

With Business Central 2026 Release Wave 1 (BC28), Microsoft has simplified this workflow. You can now create purchase orders directly from the sales order itself using the Create Purchase Orders action. On top of that, drop shipment lines are now included in the Order Planning page when you calculate plans, and the Planning Worksheet page gets a dedicated Drop Shipment action group.


What Exactly Changed?

1. Create Purchase Orders Directly from Sales Orders

Previously, to create a purchase order for a drop shipment, you had to leave the sales order, go to the purchase side, and manually pull in the sales lines. Now there is a Create Purchase Orders action right on the sales order. Business Central looks at your drop shipment lines, figures out the vendor from the Item Card or Stockkeeping Unit Card, and suggests the purchase order lines for you. You review, tweak if needed, and you are done. 







2. Drop Shipments Included in Order Planning

The Order Planning page now considers drop shipment lines when you run Calculate Plan. Before this update, drop shipment demand was essentially invisible to the planning engine. Now it shows up alongside your other demand, which gives you a more complete picture of what you need to order and when.


3. New Drop Shipment Actions in Planning Worksheet

The Planning Worksheet page now includes a dedicated Drop Shipment action group with Get Sales Orders and Sales Orders actions. This mirrors what the requisition worksheet already had, bringing consistency across the planning tools.





Tip: Vendor Determination

Business Central picks the vendor from the Item Card's Vendor No. field first. If you use Stockkeeping Units (SKUs), the vendor configured on the SKU for that location takes priority. You can always override it on the Create Purchase Orders page before confirming.


Before vs. After: Quick Comparison

 

Area

Before (BC27 and earlier)

After (BC28)

Creating POs from Sales Orders

Create PO separately, then use Get Sales Orders from the purchase order

Create Purchase Orders action directly on the sales order

Order Planning

Drop shipment lines not included in Calculate Plan

Drop shipment lines are included when calculating plans

Planning Worksheet

No dedicated drop shipment actions; only available in requisition worksheet

New Drop Shipment action group with Get Sales Orders and Sales Orders actions



Sunday, 8 March 2026

Define Item Attributes for Item Variants

The public preview for Dynamics 365 Business Central 2026 release wave 1 (BC28) is now available, and there’s a feature in this release that many of us in the BC community have been waiting for. You can now define item attributes at the item variant level. It sounds simple, but if you’ve ever worked with variants in a real-world scenario, you know how much this changes things.

Let me walk you through what this feature does, how it works, and why it matters.


The Problem This Solves

Item variants have been a great way to keep your product list manageable in Business Central. Instead of creating a separate item for every color, size, or material variation, you set up one item and define the variants underneath it. A t-shirt in five colors? One item, five variants. Much cleaner than five separate item cards.

But here’s where it used to fall short: item attributes. Until now, attributes were only defined at the item level. So if your base t-shirt had an attribute of “Color: Blue,” all your variants inherited that same value. The red variant? Also said “Blue.” Not ideal.

You either had to live with inaccurate attribute data on your variants, or you gave up on variants entirely and created separate items just so each one could have the right attribute values. Neither option was great.

What’s New in BC28

With this release, you can assign attribute values that reflect the specific variant at the item variant level. Variants still inherit attributes from the parent item, but you can now adjust or remove those inherited values when a variant needs different information.

Here’s what that looks like in practice:

Key Capabilities:

      Assign attribute values directly to individual item variants

      Variants inherit attributes from the parent item automatically

      Adjust or remove inherited values per variant as needed

      New Item Variant Attribute Values page accessible from the Variants List or Variant Card

      New Update Variant Attributes action on the Item Card to force sync from item to variants


How It Works

Attribute Inheritance

When you add a new variant to an item, the attributes defined for that item automatically transfer to the variant. This is the inheritance part. If you’ve already set up attributes like Material, Weight, or Color on the parent item, your new variant gets all of those right away.




One thing to note: if no attributes are defined on the parent item, the variants will also be empty. The inheritance only works when there’s something to inherit.

Editing Variant-Specific Attributes

Once the attributes are inherited, you can open the new Item Variant Attribute Values page to review and edit them. You can get to this page from either the Item Variants List or the Item Variant Card. From there, you update or delete the inherited values so each variant shows the correct details.

So back to our t-shirt example: your parent item might have Color set to “Blue” (the default). When you create the “Red” variant, it inherits “Blue,” but you can immediately go in and change it to “Red.” Simple, clean, accurate.


Syncing Attributes with Update Variant Attributes

There’s also a new Update Variant Attributes action available directly from the Item Card. This lets you force a sync of attributes from the item down to all its variants. It’s useful when you’ve added or changed attributes on the parent item and want to push those changes out to existing variants without going through them one by one.





Saturday, 7 March 2026

Create Purchase Quotes for Contacts in Business Central

The public preview for Dynamics 365 Business Central 2026 release wave 1 (BC28) is now available, and there are some really useful features in this update. One that caught my attention is the ability to create purchase quotes for contacts that aren't yet associated with a vendor. If you've worked in purchasing, you know the friction here. Let me walk you through it.


The Problem This Solves

Until now, if you wanted to create a purchase quote in Business Central, you needed to have a vendor record already set up. That sounds reasonable in theory, but in practice it creates unnecessary work. Think about it: you're exploring a potential purchase from a new supplier. You're not even sure the deal will go through. But before you can even draft a quote, you have to go create a full vendor record. That's wasted effort if the purchase never materializes.


How It Works

The concept is straightforward. You can now create a purchase quote and assign it to a contact that isn't linked to any vendor. Business Central lets you fill in the quote details, add your lines, and work with the document just like you normally would. The key difference is that you don't need a vendor number to get started.

Here's the step-by-step process:

1.    Create a Purchase Quote and select a contact that isn't linked to a vendor. The Purchase Quote will be created without a Vendor No., and that's perfectly fine at this stage.






2.    Choose a vendor template if multiple templates are available. Business Central selects the template based on criteria like territory code, country/region code, and contact type. 

3.    Enter the quote details as you normally would. Add your lines, quantities, prices, and any other information you need.

4.    Release the quote or convert it to an order. At this point, Business Central automatically creates the vendor record using the template. The vendor is created only when you actually need it.







Saturday, 28 February 2026

Why API Templates Don't Work on Business Central Custom API Pages

 If you've ever tried to set up an API template for a custom API page in Business Central and wondered why nothing seems to apply, you're not alone. The Microsoft documentation mentions that API templates only work with a specific list of standard pages, but it doesn't really explain why. That's what this post is about.


What Are API Templates?

API templates let you pre-populate fields on new records created via a POST call to a BC API. The idea is simple: when someone creates a record through the API and doesn't supply every field, the template fills in the blanks.

 

You set them up in API Setup page, pick a Table/Page ID and a Template Code from your Configuration Templates, and optionally add conditions. Done or so you'd think.

 

The catch is in the docs:

 

API templates can only be set up with the following API pages: contacts, countriesRegions, currencies, customers, employees, itemCategories, paymentMethods, paymentTerms, shipmentMethods, unitsOfMeasure, and vendors.

 

 

API Pages That Support Templates (Out of the Box)

contacts

countriesRegions

currencies

customers

employees

itemCategories

paymentMethods

paymentTerms

shipmentMethods

unitsOfMeasure

vendors


Why Only Those Pages?

Microsoft's docs don't explain the reason, but the answer is in the code.

 

Every one of those supported pages calls a specific function in the OnInsertRecord trigger:

 

GraphMgtGeneralTools.ProcessNewRecordFromAPI(CustomerRecordRef, TempFieldSet, CurrentDateTime());

 

This is the function that actually looks up your API Setup configuration and applies the template values. Without it, the framework has no hook to do anything. The template gets ignored completely.

 

Here's the full OnInsertRecord from the standard Customer API page so you can see exactly how it's wired up:

 

trigger OnInsertRecord(BelowxRec: Boolean): Boolean

var

    Customer: Record Customer;

    CustomerRecordRef: RecordRef;

begin

    if Rec.Name = '' then

        Error(NotProvidedCustomerNameErr);

 

    Customer.SetRange("No.", Rec."No.");

    if not Customer.IsEmpty() then

        Rec.Insert();

 

    Rec.Insert(true);

 

    CustomerRecordRef.GetTable(Rec);

    GraphMgtGeneralTools.ProcessNewRecordFromAPI(CustomerRecordRef, TempFieldSet, CurrentDateTime());

    CustomerRecordRef.SetTable(Rec);

 

    Rec.Modify(true);

    SetCalculatedFields();

    exit(false);

end;

 

The key steps are:

1.    Insert the record first (so it exists in the DB)

2.    Get a RecordRef from it

3.    Call ProcessNewRecordFromAPI, this is where the template lookup happens

4.    Set the record back from the RecordRef so updated values come through

5.    Modify the record to save everything

 

Custom API pages and the Resource API page don't have this plumbing, so templates never apply.


Seeing the Problem in Action

Let's prove this with the Resource API page. First, create a simple Configuration Template for Resources, say, one that sets a default Base Unit of Measure to HOUR.

 



Go to API Setup, add a new line, pick the Resource table or API page, and assign your template. Looks fine.



Now build Custom API Page from Scratch


page 50200 "My Custom Resource API"

{

    PageType = API;

    APIPublisher = 'mohana';

    APIGroup = 'ap';

    APIVersion = 'v2.0';

    EntityName = 'resource';

    EntitySetName = 'resources';

    SourceTable = Resource;

    ODataKeyFields = SystemId;

    InsertAllowed = true;

    DeleteAllowed = false;

    ModifyAllowed = true;

 

    layout

    {

        area(Content)

        {

            repeater(Group)

            {

                field(id; Rec.SystemId) { }

                field(number; Rec."No.") { }

                field(displayName; Rec.Name) { }

                field(baseUnitOfMeasure; Rec."Base Unit of Measure") { }

                field(type; Rec.Type) { }

            }

        }

    }

}

 

Now POST a new resource via the API without specifying Base Unit of Measure:

 

POST /api/v2.0/companies({id})/resources

{

  "number": "RES-TEST-01",

  "displayName": "Test Resource"

}

 

Check the record that gets created. The Base Unit of Measure field is empty. The template did nothing.



That's because the Resource API page's OnInsertRecord has no ProcessNewRecordFromAPI code.


The Fix: Add the Missing code


page 50200 "My Custom Resource API"

{

    PageType = API;

    APIPublisher = 'mohana';

    APIGroup = 'ap';

    APIVersion = 'v2.0';

    EntityName = 'resource';

    EntitySetName = 'resources';

    SourceTable = Resource;

    ODataKeyFields = SystemId;

    InsertAllowed = true;

    DeleteAllowed = false;

    ModifyAllowed = true;

 

    layout

    {

        area(Content)

        {

            repeater(Group)

            {

                field(id; Rec.SystemId) { }

                field(number; Rec."No.") { }

                field(displayName; Rec.Name) { }

                field(baseUnitOfMeasure; Rec."Base Unit of Measure") { }

                field(type; Rec.Type) { }

            }

        }

    }

 

    var

        GraphMgtGeneralTools: Codeunit "Graph Mgt - General Tools";

        TempFieldSet: Record 2000000041 temporary;

 

    trigger OnInsertRecord(BelowxRec: Boolean): Boolean

    var

        ResourceRecordRef: RecordRef;

    begin

        if Rec.Name = '' then

            Error('Resource display name is required.');

 

        Rec.Insert(true);

 

        ResourceRecordRef.GetTable(Rec);

        GraphMgtGeneralTools.ProcessNewRecordFromAPI(

            ResourceRecordRef, TempFieldSet, CurrentDateTime());

        ResourceRecordRef.SetTable(Rec);

 

        Rec.Modify(true);

        exit(false);

    end;

}

 

With the extension published, run the same POST call again:

 

POST /api/v2.0/companies({id})/resources

{

  "number": "RES-TEST-02",

  "displayName": "Test Resource 2"

}

 

This time, open the resource record. You'll see Base Unit of Measure is populated with HOUR from the template, even though you didn't pass it in the POST body.

 


The template is working exactly as it should. The only difference is the three lines of code that hook into the template engine.


The TempFieldSet Variable

TempFieldSet is a temporary record based on the Field system table (table 2000000041). The ProcessNewRecordFromAPI function uses it to track which fields were explicitly set in the API call (as opposed to defaulted). This is how it knows which fields to leave alone and which to fill in from the template.

 

BC does not populate TempFieldSet automatically. You have to register each field manually by calling RegisterFieldSet from the OnValidate trigger of every field on your API page. If you skip this, ProcessNewRecordFromAPI has no record of which fields the caller actually sent, and it will treat all of them as update, meaning the template could overwrite values the API caller explicitly passed in.

 

Here's the pattern you need on every field:

 

field(type; Rec."Contact Type")

{

    Caption = 'Type';

    trigger OnValidate()

    begin

        RegisterFieldSet(Rec.FieldNo("Contact Type"));

    end;

}

 

And the RegisterFieldSet procedure itself:

 

local procedure RegisterFieldSet(FieldNo: Integer)

begin

    if TempFieldSet.Get(Database::Customer, FieldNo) then

        exit;

    TempFieldSet.Init();

    TempFieldSet.TableNo := Database::Customer;

    TempFieldSet.Validate("No.", FieldNo);

    TempFieldSet.Insert(true);

end;

 

The Get check at the top prevents duplicate entries if the same field gets validated more than once. Every field you expose on the API page needs its own OnValidate trigger calling this procedure. If you miss a field, the template will overwrite whatever the caller sent for that field.


What If You Pass a Value That's Also in the Template?

The API caller always wins. ProcessNewRecordFromAPI only applies a template value if that field is not already in TempFieldSet. Since every field you explicitly send in the POST body gets tracked there, the function simply skips those fields when applying the template.

 


So if your template sets Type to Machine but your POST includes "type": "Person", the record gets Machine without TempFieldSet.


If your template sets Type to Machine but your POST includes "type": "Person", the record gets Person with TempFieldSet.

The template value is ignored for that field.


This matches what the Microsoft docs say about template ordering too:

 

Field values defined in the template are only applied to fields that have not already had a value defined, either explicitly in the API, or in a previously applied template in the order.

 

The same rule applies when you have multiple templates stacked in API Setup with different order numbers. The first template to set a field wins, and any later templates skip it.

 

Priority

Source

Wins over

1 (highest)

Explicit API value

Everything

2

Earlier template (lower Order #)

Later templates

3 (lowest)

Later template (higher Order #)

Nothing