Adding Custom Credit Max Check Type in D365 Finance & Operations

By Deovandski Skibinski | June 3, 2026

Out of the box, Dynamics 365 Finance & Operations provides standard Credit max check options, but some implementations need a more specific calculation model. A common requirement is to evaluate credit exposure based on released sales orders that have not yet been invoiced, instead of relying only on the standard balance, packing slip, or invoice-related checks.

This post shows a complete framework for adding a new custom TypeOfCreditmaxCheck enum value and wiring it through the credit control pipeline in D365 F&O to extend the credit control framework to support a custom calculation based on released but not yet invoiced sales orders.

Base Implementation: Adding a Custom Credit Check Type

The following extensions define the new enum value and implement the core calculation logic within the CustCreditLimit framework.

Step 1: Extend the Credit Check Enum

[Enum Extension] TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced[ExtensionOf(classStr(CustCreditLimit))]

Step 2: Implement the Core Calculation Logic

[ExtensionOf(classStr(CustCreditLimit))]
CustCreditLimit_SSI_Extension
{
    AmountMST balanceReleasedNotYetInvoiced(AmountMST _balanceNotInvoiced = _balanceNotInvoiced)
    {
        if (!prmisDefault(_balanceNotInvoiced))
        {
            BalanceReleasedNotYetInvoiced = _balanceNotInvoiced;
            BalanceReleasedNotYetInvoicedCalculated = true;
        }
        else
        {
            if (!BalanceReleasedNotYetInvoicedCalculated)
            {
                BalanceReleasedNotYetInvoiced = this.calcBalanceReleasedNotYetInvoiced();
                BalanceReleasedNotYetInvoicedCalculated = true;
            }
        }
        return BalanceReleasedNotYetInvoiced;
    }

    protected AmountMST calcBalanceReleasedNotYetInvoiced()
    {
        SalesTable salesTableLocal;
        SalesLine salesLine;
        CredManTrans credManTrans;
        AmountMST ret;
        CredManCreditLimitId credManGroup = this.credManCreditLimitCustGroup(custTable.AccountNum);
        RollupView RollupView;

        if(credManGroup == '')
        {
            select sum(Total) from RollupView
                where RollupView.InvoiceAccount == custTable.InvoiceAccount;
            ret += RollupView.Total;
        }
        else // we have a credit group and we need to account for the added accounts
        {
            CredManCreditLimitCustGroupView credManGroupView;

            while select credManGroupView where credManGroupView.CreditLimitId == credManGroup
            {
                select sum(Total) from RollupView
                where RollupView.InvoiceAccount == credManGroupView.AccountNum;
                ret += RollupView.Total;
            }
        }
        return ret;
    }

    internal CredManCreditLimitId credManCreditLimitCustGroup(CustAccount _custAccount)
    {
        CredManCreditLimitId credManCreditLimitId;
        CredManCreditLimitCustGroupView credManCreditLimitCustGroupView;

        if (CredManParameterManagementVisibilityManager::isEnabled())
        {
            select firstonly CreditLimitId from credManCreditLimitCustGroupView
            where credManCreditLimitCustGroupView.AccountNum == _custAccount
               && credManCreditLimitCustGroupView.Company == curExt();

            credManCreditLimitId = credManCreditLimitCustGroupView.CreditLimitId;
        }
        return  credManCreditLimitId;
    }

    protected AmountMST amountAddedToBalanceBasedOnTypeOfCreditmaxCheck()
    {
        AmountMST amountToAdd = next amountAddedToBalanceBasedOnTypeOfCreditmaxCheck();
        switch (this.typeOfCreditMaxCheck())
        {
            case TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced:
            amountToAdd = this.calcBalanceReleasedNotYetInvoiced();
            break;
        }
        return amountToAdd;
    }
}

Step 3: Apply the Calculation Across Companies

[ExtensionOf(classStr(CredManCustCreditLimitCrossCompany))]
CredManCustCreditLimitCrossCompany_SSI_Extension
{
    protected AmountMST amountAddedToBalanceBasedOnTypeOfCreditmaxCheck(CustCreditLimit _custCreditLimit)
    {
        AmountMST balance = next amountAddedToBalanceBasedOnTypeOfCreditmaxCheck(_custCreditLimit);

        switch (_custCreditLimit.typeOfCreditMaxCheck())
        {
            case TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced:
                balance = _custCreditLimit.balanceInvoiced() + _custCreditLimit.balanceDelivered() + _custCreditLimit.balanceReleasedNotYetInvoiced();
                break;
        }

        return balance;
    }
}

Step 4: Extend Vendor Credit Checks

[ExtensionOf(tableStr(VendTable))]
final class VendTable_SSI_Extension
{
  protected static boolean checkCreditLimitForCreditMaxCheck(VendAccount _vendAccount,
        TypeOfCreditmaxCheck        _check,
        AmountMST                   _openBalanceMST,
        AmountMST                   _agreementCreditLimitMax,
        boolean                     _checkAgreementLimit,
        AmountMST                   _amountMST,
        PurchId                     _excludePurchId,
        AgreementHeaderExtRecId_RU  _agreementHeaderExtRecId)
    {

        boolean ret = next checkCreditLimitForCreditMaxCheck(_vendAccount, _check,
            _openBalanceMST, _agreementCreditLimitMax,_checkAgreementLimit,
            _amountMST, _excludePurchId, _agreementHeaderExtRecId);
       
        AmountMST purchBalanceMst;
        switch (_check)
        {
            // Default to Balance option unless further mod is needed.
            case TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced:
                purchBalanceMst = PurchTable::balanceAmountNotInvoiced(_vendAccount, _excludePurchId, _checkAgreementLimit, _agreementHeaderExtRecId) + _amountMST;
                if (_openBalanceMST + purchBalanceMst > _agreementCreditLimitMax)
                {
                    ret = checkFailed(strFmt("@SYS25668", _openBalanceMST, purchBalanceMst,
                    _openBalanceMST + purchBalanceMst, _agreementCreditLimitMax,
                        (_openBalanceMST + purchBalanceMst) - _agreementCreditLimitMax));
                }
                else
                {
                    ret = true;
                }
                break;
        }
        return ret;
    }  
}

Step 5: Adjust Estimated Credit Behavior

[ExtensionOf(tableStr(CustTable))]
Final class SSICustTable_Extension
{
   public boolean isEstimatedBasedOnCreditMaxCheck()
    {
        boolean ret = next isEstimatedBasedOnCreditMaxCheck();
        CustParameters custParameters = CustParameters::find();

        if(custParameters.CreditMaxCheck == TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced)
        {
            return false;
        }
        else
        {
            return ret;
         }
    }
}

Handling Advanced Scenarios and Transaction Context

The base implementation shown above is enough when your custom credit exposure calculation can be derived from shared aggregate data without needing transaction context.
If your logic must consider the current sales order, invoice, or posting batch being processed, you will likely need additional extensions on the specialized CustCreditLimit_* classes.
This is necessary when the current document is already included in your aggregate source and must be excluded during validation to prevent double counting.
In practice, that usually means overriding the custom calculation in classes such as:
  • CustCreditLimit_SalesTable
  • CustCreditLimit_CustInvoiceTable
  • CustCreditLimit_SalesParmTable
Each override can identify the current sales order context and subtract its value from the aggregated result before returning the final exposure amount.

Extending for Transaction-Specific Adjustments

The following extensions demonstrate how to exclude the current transaction from the aggregated balance to prevent double counting during validation.

Cross-Company Routing Logic

[ExtensionOf(classStr(CredManCustCreditLimitCrossCompany))]
final class CredManCustCreditLimitCrossCompany_SSI_Extension
{
    protected AmountMST amountAddedToBalanceBasedOnTypeOfCreditmaxCheck(CustCreditLimit _custCreditLimit)
    {
        AmountMST balance = next amountAddedToBalanceBasedOnTypeOfCreditmaxCheck(_custCreditLimit);

        switch (_custCreditLimit.typeOfCreditMaxCheck())
        {
            case TypeOfCreditmaxCheck::BalanceReleasedNotYetInvoiced:
                if (_custCreditLimit is CustCreditLimit_SalesParmTable)
                {
                    CustCreditLimit_SalesParmTable custCreditLimitSalesParm = _custCreditLimit;
                    balance =  _custCreditLimit.calcBalanceReleasedNotYetInvoiced();
                }
                else if (_custCreditLimit is CustCreditLimit_CustInvoiceTable)
                {
                    CustCreditLimit_CustInvoiceTable custCreditLimitSales = _custCreditLimit;
                    balance =  _custCreditLimit.calcBalanceReleasedNotYetInvoiced();
                }
                else if (_custCreditLimit is CustCreditLimit_SalesTable)
                {
                    CustCreditLimit_SalesTable custCreditLimitSales = _custCreditLimit;
                    balance =  _custCreditLimit.calcBalanceReleasedNotYetInvoiced();
                }
                else
                {
                    balance =  _custCreditLimit.calcBalanceReleasedNotYetInvoiced();
                }
                break;
        }

        return balance;
    }
}

Sales Order-Specific Logic

[ExtensionOf(classStr(CustCreditLimit_SalesTable))]
final class CustCreditLimit_SalesTable_SSI_Extension
{

    public AmountMST calcBalanceReleasedNotYetInvoiced()
    {
        SalesTable salesTableLocal;
        SalesLine salesLine;
        CredManTrans credManTrans;
        AmountMST ret;
        CredManCreditLimitId credManGroup = this.credManCreditLimitCustGroup(custTable.AccountNum);
        RollupView RollupView;

        if(credManGroup == '')
        {
            select sum(Total) from RollupView
                where RollupView.InvoiceAccount == custTable.InvoiceAccount;
            ret += RollupView.Total;
        }
        else
        {
            CredManCreditLimitCustGroupView credManGroupView;

            while select credManGroupView where credManGroupView.CreditLimitId == credManGroup
            {
                select sum(Total) from RollupView
                where RollupView.InvoiceAccount == credManGroupView.AccountNum;
                ret += RollupView.Total;
            }
        }
        SalesTable salesTable;
        if(this.salesTable)
        {
            salesTable = SalesTable::find(this.salesTable.SalesId);
        }
        if(salesTable)
        {
            ret -= SSICustCreditLimitHelper::getSalesTotals(salesTable);
        }
        return ret;
    }
}

Invoice-Specific Logic

[ExtensionOf(classStr(CustCreditLimit_SalesTable))]
final class CustCreditLimit_CustInvoiceTable_SSI_Extension
{
    public AmountMST calcBalanceReleasedNotYetInvoiced()
    {  
        SalesTable salesTableLocal;
        SalesLine salesLine;
        CredManTrans credManTrans;
        AmountMST ret;
        CredManCreditLimitId credManGroup = this.credManCreditLimitCustGroup(custTable.AccountNum);
        RollupView RollupView;

        if(credManGroup == '')
        {
            select sum(Total) from RollupView
                where RollupView.InvoiceAccount == custTable.InvoiceAccount;
            ret += RollupView.Total;
        }
        else
        {
            CredManCreditLimitCustGroupView credManGroupView;

            while select credManGroupView where credManGroupView.CreditLimitId == credManGroup
            {
                select sum(Total) from RollupView
                where RollupView.InvoiceAccount == credManGroupView.AccountNum;
                ret += RollupView.Total;
            }
        }
        SalesTable salesTable;
        if(this.custInvoiceTable && this.custInvoiceTable.SalesId)
        {
            salesTable = SalesTable::find(this.custInvoiceTable.SalesId);
        }
        if(salesTable)
        {
            ret -= SSICustCreditLimitHelper::getSalesTotals(salesTable);
        }
        return ret;
    }
}

Posting Parameter / Batch Logic

[ExtensionOf(classStr(CustCreditLimit_SalesParmTable))]
final class CustCreditLimit_SalesParmTable_SSI_Extension
{
    public AmountMST calcBalanceReleasedNotYetInvoiced()
    {  
        SalesTable salesTableLocal;
        SalesLine salesLine;
        CredManTrans credManTrans;
        AmountMST ret;
        CredManCreditLimitId credManGroup = this.credManCreditLimitCustGroup(custTable.AccountNum);
        RollupView RollupView;

        if(credManGroup == '')
        {
            select sum(Total) from RollupView
                where RollupView.InvoiceAccount == custTable.InvoiceAccount;
            ret += RollupView.Total;
        }
        else
        {
            CredManCreditLimitCustGroupView credManGroupView;

            while select credManGroupView where credManGroupView.CreditLimitId == credManGroup
            {
                select sum(Total) from RollupView
                where RollupView.InvoiceAccount == credManGroupView.AccountNum;
                ret += RollupView.Total;
            }
        }
        SalesTable salesTable;
        if(this.salesTable)
        {
            salesTable = SalesTable::find(this.salesTable.SalesId);
            if(salesTable)
            {
                ret -= SSICustCreditLimitHelper::getSalesTotals(salesTable);
            }
        }
        else if(this.salesParmTable)
        {
            SalesParmTable salesParmTable;
            while select salesParmTable where salesParmTable.ParmId == this.salesParmTable.ParmId
            {
                salesTable = SalesTable::find(salesParmTable.SalesId);
                if(salesTable)
                {
                    ret -= SSICustCreditLimitHelper::getSalesTotals(salesTable);
                }
            }
        }
        return ret;
    }
}

Key Takeaways

Extending credit management in Dynamics 365 Finance & Operations allows you to tailor credit exposure calculations to better reflect how your business actually operates. By introducing a custom 'TypeOfCreditmaxCheck', you can account for release but not yet invoiced sales orders.

If you're evaluating a similar requirement or need help designing a custom credit control approach in D365 F&O, the Stoneridge team can help.


Request Your Software Consultation CTA Button

Deovandski Skibinski
Our Verified Expert
Deovandski Skibinski

Deovandski Skibinski is a developer with experience across multiple programming languages and recent expertise in X++ development for Dynamics 365 Finance and Supply Chain. He brings a passion for problem solving client challenges and enjoys sharing knowledge with fellow developers. His work spans retail, distribution, inventory, and finance, where he builds reliable, efficient solutions that help businesses run smarter. Deo holds a bachelor’s degree in Computer Science from North Dakota State University.

Read More from Deovandski Skibinski

Related Posts


Under the terms of this license, you are authorized to share and redistribute the content across various mediums, subject to adherence to the specified conditions: you must provide proper attribution to Stoneridge as the original creator in a manner that does not imply their endorsement of your use, the material is to be utilized solely for non-commercial purposes, and alterations, modifications, or derivative works based on the original material are strictly prohibited.

Responsibility rests with the licensee to ensure that their use of the material does not violate any other rights.

Start the Conversation

It’s our mission to help clients win. We’d love to talk to you about the right business solutions to help you achieve your goals.

Subscribe To Our Blog

Sign up to get periodic updates on the latest posts.

Thank you for subscribing!