Monday, 28 September 2026

X++ client and server Keywords Deprecated in D365 F&O 10.0.50

Microsoft is continuing to modernize the X++ language in Dynamics 365 Finance & Operations by removing legacy concepts that are no longer relevant to the current platform architecture.

One such change affects the client and server keywords used in X++ method declarations.

These keywords have been obsolete from a runtime perspective for many years, but they have remained in existing X++ code. Microsoft has now started the process of removing them from the language.

If you maintain an older Dynamics AX or D365 F&O codebase, this is something you should check before moving your development environment to the newer platform versions.

What Is Changing?

Historically, X++ developers could use client and server modifiers to specify where a method should execute.

For example:

public client static void updateCustomer()
{
// Method logic
}

or:

public server void processOrder()
{
// Method logic
}

With the modern Dynamics 365 Finance & Operations architecture, these modifiers no longer control the execution tier of the method.

Microsoft explains that X++ methods currently execute on the server tier, making the old modifiers unnecessary. Starting with platform update 10.0.48, the compiler began reporting these keywords as warnings and Microsoft has instructed developers to remove them from existing code and avoid using them in new development.

The next important step is 10.0.50, where their use results in compilation errors.

The transition looks like this:

Platform versionBehavior
10.0.48client and server generate compiler warnings
10.0.50Their use causes compiler errors
Future versionsThe keywords are expected to be removed completely from the X++ grammar

This makes the change particularly important for teams maintaining large customizations or legacy code.

Why Were These Keywords Deprecated?

The client and server modifiers made sense in the older AX architecture, where developers needed to distinguish between client-side and server-side execution.

The architecture of Dynamics 365 Finance & Operations is different.

As Microsoft notes, these keywords no longer determine where the method executes. Consequently, keeping them in the language adds legacy syntax without providing meaningful runtime behavior.

For example, this:

public client static void updateCustomer()
{
// Update customer
}

can simply become:

public static void updateCustomer()
{
// Update customer
}

The client keyword is removed, but the method itself remains unchanged.

Similarly:

protected server void calculateAmount()
{
// Calculate amount
}

becomes:

protected void calculateAmount()
{
// Calculate amount
}

There is no need to redesign the method merely because the obsolete modifier is being removed.

Does Removing client or server Change the Method's Behavior?

In general, no.

The important point is that these modifiers are no longer responsible for determining execution location in the current D365 F&O architecture.

Therefore, removing the modifier is primarily a source-code cleanup and compiler-compatibility change, rather than a functional change to the business logic.

For example:

Before

public client static void createCustomer()
{
// Business logic
}

After

public static void createCustomer()
{
// Business logic
}

The method's access modifier, return type, name, parameters, and implementation remain intact.

Only the obsolete keyword is removed.

How to Find These Keywords in an Existing Project

If you are working with a solution that has been upgraded from AX 2012 or an older D365 F&O implementation, it is worth scanning the X++ codebase.

A basic search for:

client
server

can help identify potential occurrences.

However, don't blindly remove every occurrence.

The words may appear in:

  • Comments

  • String literals

  • Variable names

  • Documentation

  • Other code where they are not being used as method modifiers

What you are specifically looking for is a method declaration such as:

public client static void methodName()

or:

protected server void methodName()

The relevant change is to remove only the obsolete modifier.

Recommended Cleanup Process

Before upgrading your development environment to a platform version where these keywords produce compilation errors, I recommend the following approach.

1. Search the X++ codebase

Search your custom models and projects for:

client
server

2. Review the results

Do not perform a simple find-and-replace.

Verify that each occurrence is actually being used as an X++ method modifier.

3. Remove the obsolete modifiers

For example:

public client static void process()

becomes:

public static void process()

And:

private server void calculate()

becomes:

private void calculate()

4. Compile the affected projects

After making the changes, perform a build and check for any remaining compiler warnings or errors.

5. Run your normal validation

Although this particular change should not alter business logic, your normal build and regression-testing process should still be followed.

Microsoft's guidance on D365 F&O updates highlights that some platform changes can introduce design-time compilation issues even when runtime/binary compatibility is maintained.

What About New X++ Development?

For new development, there is no reason to add these modifiers.

Avoid writing:

public client static void myMethod()

or:

public server void myMethod()

Instead, use the normal X++ method declaration:

public static void myMethod()

or:

public void myMethod()

This keeps your code aligned with the current X++ language direction and avoids unnecessary compiler warnings or future compatibility issues.

A Small Change That Can Affect Large Codebases

The actual code change is very small.

The challenge is more likely to be the size of the codebase.

Large D365 F&O implementations can contain:

  • Years of custom X++ development

  • AX 2012 migrated code

  • ISV customizations

  • Extensions

  • Legacy integrations

  • Utility classes

  • Old framework customizations

A project may therefore contain these modifiers in places that developers no longer actively maintain.

That is why it is better to identify and clean them up proactively instead of waiting for the platform upgrade to turn them into compilation errors.

Important Point for AX 2012 Developers

Developers coming from AX 2012 may remember that client and server were meaningful concepts when deciding where X++ code executed.

That historical behavior is exactly why these keywords can still be found in older codebases.

However, developers should not assume that the presence of these keywords today means the method is being forced onto a particular tier.

Microsoft's current documentation explicitly states that the keywords were useful in AX 2012, but in the current architecture they no longer make sense because methods execute on the server tier.

Quick Example

Legacy Code

public client static void processSalesOrder(SalesId _salesId)
{
// Process sales order
}

Updated Code

public static void processSalesOrder(SalesId _salesId)
{
// Process sales order
}

Another example:

Legacy Code

protected server Amount calculateTotal()
{
// Calculate total
}

Updated Code

protected Amount calculateTotal()
{
// Calculate total
}

In both cases, the business logic remains untouched.

Final Thoughts

The deprecation of client and server is a relatively small X++ language change, but it is worth addressing early if you maintain a large or legacy D365 F&O codebase.

The key takeaway is simple:

client and server should no longer be used as X++ method modifiers.

If your existing code still contains them, remove the obsolete modifiers, compile the affected projects, and address any resulting compiler diagnostics before moving to the affected platform version.

For developers maintaining older AX/D365 F&O implementations, this is also a good opportunity to clean up legacy X++ syntax and keep customizations aligned with the current platform architecture.

Friday, 18 September 2026

How to Parse Excel Dates in D365 F&O Using X++

When importing data from Excel into Dynamics 365 Finance & Operations (D365 F&O), dates can come in different formats. Depending on how the Excel file was created or how EPPlus reads the cell, the value may be returned as a System.DateTime, a numeric Excel serial date, or a string.

The following helper method handles these common scenarios and converts the Excel value into an X++ date.

X++ Code

private static date parseExcelDate(OfficeOpenXml.ExcelRange _cells, int _row, int _col)
{
    System.Object   cellValue = _cells.get_Item(_row, _col).Value;
    date            result    = dateNull();

    if (cellValue == null)
    {
        return result;
    }

    System.Type     valueType = cellValue.GetType();
    str             typeName  = valueType.FullName;

    if (typeName == 'System.DateTime')
    {
        System.DateTime dateTime = cellValue;
        result = mkDate(dateTime.Day, dateTime.Month, dateTime.Year);
    }
    else if (typeName == 'System.Double' ||
             typeName == 'System.Int32' ||
             typeName == 'System.Decimal')
    {
        // Cell holds a raw OLE Automation date serial number
        real            oaDate   = any2real(cellValue);
        System.DateTime dateTime = System.DateTime::FromOADate(oaDate);

        result = mkDate(
            dateTime.Day,
            dateTime.Month,
            dateTime.Year);
    }
    else
    {
        try
        {
            result = str2Date(any2Str(cellValue), 321);
        }
        catch
        {
            warning(strFmt(
                "Could not parse date value at row %1, column %2 (type: %3).",
                _row,
                _col,
                typeName));

            result = dateNull();
        }
    }

    return result;
}

How It Works

The method first reads the Excel cell value and checks its .NET type.

  • System.DateTime – Converts the .NET DateTime directly into an X++ date.

  • Numeric values – Excel may store dates as OLE Automation serial numbers. System.DateTime::FromOADate() converts these values into a .NET DateTime.

  • String values – Attempts to convert the value using str2Date().

  • Empty or invalid values – Returns dateNull() and displays a warning when the value cannot be parsed.

Example

You can call the method while processing Excel rows:

date deliveryDate = parseExcelDate(cells, row, 5);

This approach is useful when building Excel import functionality in D365 F&O, especially when the same import file may contain dates represented in different formats.

Tip: Avoid assuming that an Excel date will always be returned as a string. Excel frequently stores dates internally as numeric serial values, which is why handling System.Double, System.Int32, and System.Decimal can prevent unexpected date conversion issues.

Monday, 31 August 2026

Create a Purchase Order from a Sales Order Using PurchCreateFromSalesOrder Framework in X++ | D365 F&O

In Dynamics 365 Finance & Operations, you may need to automatically create a Purchase Order from a Sales Order for scenarios such as intercompany processing, direct delivery, or automated procurement.

Instead of manually creating PurchTable and PurchLine, we can use the standard PurchCreateFromSalesOrder and PurchAutoCreate framework.

public static PurchId processDirectDeliveryPO (SalesTable _salesTable,  VendAccount _vendAccount)
{

    TmpPurchLinePrice         tmpPurchLinePrice;
    PurchCreateFromSalesOrder purchCreateFromSalesOrder;
    PurchAutoCreate           purchAutoCreate;
    SalesLine                 salesLine;
    PurchId                   purchId;

    delete_from tmpPurchLinePrice;

    while select salesLine
        where salesLine.SalesId == _salesTable.SalesId
    {
        if (!salesLine.ItemId)
        {
            continue;
        }

        if (salesLine.DSS_CreateFromSalesOrderConfirm != NoYes::No)
        {
            continue;
        }

        if (salesLine.referencedPurchLine().RecId)
        {
            continue;
        }

        tmpPurchLinePrice.clear();

        tmpPurchLinePrice.SalesId           = salesLine.SalesId;
        tmpPurchLinePrice.SalesLineRefRecId = salesLine.RecId;
        tmpPurchLinePrice.AccountNum        = _vendAccount;
        tmpPurchLinePrice.ItemId            = salesLine.ItemId;
        tmpPurchLinePrice.InventDimId       = salesLine.InventDimId;
        tmpPurchLinePrice.PurchQty          = salesLine.SalesQty;
        tmpPurchLinePrice.PurchUnit         = salesLine.SalesUnit;
        tmpPurchLinePrice.Included          = NoYes::Yes;

        tmpPurchLinePrice.insert();
    }

    if (!tmpPurchLinePrice.RecId)
    {
        throw error(strFmt(
            "No eligible Sales Lines found for vendor %1.",
            _vendAccount));
    }

    ttsBegin;

    purchCreateFromSalesOrder =
        PurchCreateFromSalesOrder::construct();

    purchCreateFromSalesOrder.parmCallerRecord(_salesTable);
    purchCreateFromSalesOrder.parmSalesTable(_salesTable);

    purchCreateFromSalesOrder.tradeLineDlvType(
        TradeLineDlvType::DropShip);

    purchCreateFromSalesOrder.parmTransferAddress(true);
    purchCreateFromSalesOrder.mcrDropShipment(true);

    purchAutoCreate =
        PurchAutoCreate::construct(
            tmpPurchLinePrice,
            purchCreateFromSalesOrder);

    purchAutoCreate.create();

    purchId = purchAutoCreate.purchId();

    ttsCommit;

    return purchId;
}

How It Works

The code loops through the Sales Order lines and adds only eligible lines to TmpPurchLinePrice.

It skips:

  • Lines without an item.

  • Lines that are already processed.

  • Lines already linked to a Purchase Line.

The PurchCreateFromSalesOrder framework is then configured for a DropShip/Direct Delivery scenario.

Finally, PurchAutoCreate uses the temporary records and standard D365 F&O logic to create the Purchase Order and its lines.

Where Can You Use This?

This approach can be useful for:

  • Intercompany Purchase Order creation

  • Direct Delivery / Drop Shipment

Conclusion

Using PurchCreateFromSalesOrder and PurchAutoCreate helps leverage standard D365 F&O functionality instead of manually creating PurchTable and PurchLine records. This makes the solution cleaner and easier to maintain.

Thursday, 27 August 2026

How to Access Extension Fields Dynamically in D365FO Without a Model Reference

In D365FO, sometimes a field is added through an extension in another model, but your current model cannot reference that model because of package/model dependency restrictions.

The problem is that you cannot directly access the field:

_salesTable.Custom_Field

But if you still need to read this field for data manipulation or customization, you can access it dynamically using DictTable.

Solution

public static str getExtensionFieldValue(SalesTable _salesTable)
{
    Common      commonRecord;
    DictTable   dictTable;
    FieldId     fieldId;
    str         fieldName = 'Custom_Field';
    str         fieldValue;

    dictTable = new DictTable(tableNum(SalesTable));
    fieldId = dictTable.fieldName2Id(fieldName);

    commonRecord = _salesTable as SalesTable;

    if (fieldId)
    {
        fieldValue = commonRecord.(fieldId);
    }

    return fieldValue;
}

How It Works

The code uses DictTable to find the FieldId of Custom_Field without creating a direct compile-time reference to the field.

fieldId = dictTable.fieldName2Id(fieldName);

Once the FieldId is available, the field value can be retrieved dynamically:

fieldValue = commonRecord.(fieldId);

You can then use the value in your customization:

if (getExtensionFieldValue(_salesTable) == 'Yes')
{
    // Custom logic
}

Conclusion

When a model cannot reference another model containing a table extension, dynamic field access using DictTable can be a practical way to read the extension field and continue with your required customization—without changing the existing model dependency.

Monday, 24 August 2026

How to Add a Custom JumpRef through Event Handler in D365 F&O Using X++

In Dynamics 365 Finance & Operations, a JumpRef allows users to navigate directly from a field to the related record. This is especially useful when a field contains an ID that references a record on another form.

For example, I have an Agreement ID field on the Sales Table form, and I want users to click the field and navigate directly to the corresponding Sales Agreement.

The following JumpRef event handler can be used:

[FormControlEventHandler(formControlStr(SalesTable, SalesTable_AgreementId), 
FormControlEventType::JumpRef)]
public static void SalesTable_AgreementId_OnJumpRef(FormControl sender, FormControlEventArgs e) { // Cancel the default jumpRef behavior FormControlCancelableSuperEventArgs cancelArgs = e as FormControlCancelableSuperEventArgs; if (cancelArgs != null) { cancelArgs.CancelSuperCall(); } FormRun formRun = sender.formRun() as FormRun; FormDataSource salesTableDS = formRun.dataSource(formDataSourceStr(SalesTable, SalesTable)); SalesTable salesTable = salesTableDS.cursor() as SalesTable; if (salesTable != null && salesTable.AgreementId) { SalesAgreementHeader salesAgreementHeader; // Find the Sales Agreement across all companies select firstonly crossCompany salesAgreementHeader where salesAgreementHeader.SalesNumberSequence == salesTable.AgreementId; if (salesAgreementHeader.RecId != 0) { Args args = new Args(); args.name(formStr(SalesAgreement)); // Open the record in its correct company changecompany(salesAgreementHeader.dataAreaId) { SalesAgreementHeader correctRecord; select firstonly correctRecord where correctRecord.RecId == salesAgreementHeader.RecId; args.record(correctRecord); } FormRun salesAgreementForm = classfactory.formRunClass(args); salesAgreementForm.init(); salesAgreementForm.run(); salesAgreementForm.detach(); salesAgreementForm.wait(); } else { warning(strFmt( "Sales Agreement '%1' not found in any company.", salesTable.AgreementId)); } } }

How It Works

  • Cancel default JumpRef and implement custom navigation.
  • Get the current SalesTable record and Agreement ID.
  • Use crossCompany to find the related Sales Agreement.
  • Use changecompany() to open the record in its correct legal entity.
  • Pass the record through Args and open the Sales Agreement form.
  • Show a warning if the agreement is not found.

Friday, 21 August 2026

Convert Quantity Between Units Using Unit Symbols in D365FO

 In Dynamics 365 Finance & Operations (D365FO), unit conversion is a common requirement when working with sales, purchasing, inventory, and warehouse processes.

Instead of hardcoding conversion factors, we can use the standard UnitOfMeasureConversion table to retrieve the product-specific conversion factor.

The following reusable X++ method converts a quantity from one unit to another using the configured conversion setup.

X++ Code

private Qty convertQtyByUnitSymbols(
    ItemId              _itemId,
    UnitOfMeasureSymbol _fromSymbol,
    UnitOfMeasureSymbol _toSymbol,
    Qty                 _qty)
{
    InventTable            inventTable = InventTable::find(_itemId);
    EcoResProduct          product     = inventTable.Product();
    UnitOfMeasure          fromUnit    = UnitOfMeasure::findBySymbol(_fromSymbol);
    UnitOfMeasure          toUnit      = UnitOfMeasure::findBySymbol(_toSymbol);
    UnitOfMeasureConversion conversion;
    Qty                     result      = _qty;

    if (!product.RecId || !fromUnit.RecId || !toUnit.RecId)
    {
        return result;
    }

    // Ensure the item's inventory unit matches the "from" unit
    if (inventTable.inventUnitId() != fromUnit.Symbol)
    {
        return result;
    }

    select firstonly Factor from conversion
        where conversion.Product           == product.RecId
           && conversion.FromUnitOfMeasure == fromUnit.RecId
           && conversion.ToUnitOfMeasure   == toUnit.RecId;

    if (conversion.RecId && conversion.Factor != 0)
    {
        result = _qty * conversion.Factor;
    }

    return result;
}

This approach keeps the conversion simple, reusable, and configuration-driven, using the standard D365FO unit conversion setup rather than hardcoded conversion factors.

Thursday, 20 August 2026

Check Stock Availability for Reservation in D365FO

In D365FO, before reserving inventory, you may need to verify whether the required quantity is actually available for reservation.

The following X++ method checks stock availability based on the Item, Warehouse, WMS Location, and Batch dimensions. It uses the standard InventOnhand framework to retrieve the available physical and ordered quantities.

The method then adds these quantities together and compares the result with the requested quantity. It returns true when sufficient stock is available; otherwise, it returns `false.

public static boolean isStockAvailableToReserve(
    ItemId          _itemId,
    InventLocationId _inventLocationId,
    WMSLocationId   _wmsLocationId,
    InventBatchId   _inventBatchId,
    Qty             _qty)
{
    InventDim       inventDim;
    InventDimParm   inventDimParm;
    InventOnhand    inventOnHand;

    inventDim.InventLocationId = _inventLocationId;
    inventDim.wMSLocationId    = _wmsLocationId;
    inventDim.InventBatchId    = _inventBatchId;

    inventDimParm.InventLocationIdFlag = true;
    inventDimParm.wMSLocationIdFlag    = true;
    inventDimParm.InventBatchIdFlag    = true;

    inventOnHand = InventOnhand::newParameters(
        _itemId,
        inventDim,
        inventDimParm);

    return (inventOnHand.availPhysical() +
            inventOnHand.availOrdered()) >= _qty;
}

How It Works

  • InventDim defines the inventory dimensions to check.
  • InventDimParm specifies which dimensions should be used as filters.
  • InventOnhand retrieves the inventory availability for the specified item and dimensions.
  • availPhysical() returns the currently available physical inventory.
  • availOrdered() returns the available ordered/expected quantity.
  • The method compares the combined available quantity with the requested quantity and returns a Boolean result.

This provides a simple reusable method that can be called before performing an inventory reservation in D365FO.

X++ client and server Keywords Deprecated in D365 F&O 10.0.50

Microsoft is continuing to modernize the X++ language in Dynamics 365 Finance & Operations by removing legacy concepts that are no longe...