Inactive Vendor Blocking Enhancement with Automated Email Notification

 

Purpose

The purpose of this document is to describe the design and implementation of the Automatic Blocking of Inactive Vendors functionality in Microsoft Dynamics 365 Finance and Operations (D365FO).

This enhancement automates the identification and blocking of inactive vendors based on configurable business rules, eliminating the need for manual vendor maintenance. The solution introduces a configurable batch process that evaluates vendor inactivity using the latest vendor transaction date and automatically updates the vendor blocking status.

This functionality can be used for:

  • Automatically identifying vendors with no transaction activity for the configured inactivity period (for example, 18 months).
  • Automatically updating the Vendor Blocking status to "All" for eligible inactive vendors.
  • Providing parameter-based filtering using Vendor Groups and specific Vendors.
  • Supporting batch execution across one or multiple legal entities.
  • Generating and emailing an Excel report containing all vendors blocked during the batch execution.
  • Reducing manual effort and improving data governance by ensuring inactive vendors are consistently blocked according to business policies.

Product

Microsoft Dynamics 365 Finance and Operations (D365FO)


Development Approach

The solution leverages the standard Microsoft Dynamics 365 Finance and Operations framework together with custom components.


Standard Objects

  • VendTable
  • VendTrans
  • SysOperation Framework
  • Batch Framework
  • SysMailer Framework
  • Electronic Reporting/Excel generation framework
  • Vendor Blocking (Blocked field)
  • Vendor Reasons

Custom Components

  • Contract Class
  • Controller Class
  • Service Class
  • Batch Processing Logic
  • Vendor Selection Logic
  • Vendor Group Multi-Select Lookup
  • Vendor Multi-Select Lookup
Blocked Vendor Report Generation
  • Email Notification with Excel Attachment
  • Security Role (Vendor_Blocking_MDM)



Contract class

[DataContractAttribute
, SysOperationContractProcessingAttribute(classStr( my_VendorBlockingUiBuilder))]
Public class  my_VendorBlockingContract
{
    private VendGroupId Vendgroup = "";
    private  myCustomString vendAccount;
    private TransDate fromDate;
    private TransDate toDate;
    private List      VendAccountList; // List of CustAccount strings
    int             inactivityMonths;
    ReasonCode  reasonCode;
    NoYes           applyAllLegalEntities;
    NumberOf            activityPeriod;
    NumberOf            gracePeriod;
    NoYesId             includeEmployees;
    NoYesId             excludeNewVendors;
    Email               notificationEmail;

    [DataMemberAttribute, SysOperationDisplayOrderAttribute('1')]
    public VendGroupId parmVendGroup(VendGroupId _vendGroup = Vendgroup)
    {
        Vendgroup = _vendGroup;
        return Vendgroup;
    }

    [DataMemberAttribute("Vendor Account"), SysOperationLabelAttribute(literalstr("Vendor Account")),SysOperationDisplayOrderAttribute('2')]
    public  myCustomString parmvendAccount( myCustomString _vendAccount = vendAccount)
    {
        vendAccount = _vendAccount;
        return vendAccount;
    }

    [DataMemberAttribute("@SYS300098"), SysOperationLabelAttribute(literalstr("@SYS300098")), SysOperationDisplayOrderAttribute('3')]
    public ReasonCode parmReasonCode(ReasonCode _reasonCode = reasonCode)
    {
        reasonCode = _reasonCode;

        return reasonCode;
    }

    [DataMemberAttribute('ApplyAllLegalEntities'),
     SysOperationLabelAttribute("Apply across all legal entities"),
     SysOperationDisplayOrderAttribute('4')]
    public NoYes parmApplyAllLegalEntities(NoYes _applyAllLegalEntities = applyAllLegalEntities)
    {
        applyAllLegalEntities = _applyAllLegalEntities;
        return applyAllLegalEntities;
    }

    [DataMemberAttribute('NotificationEmail'),
     SysOperationLabelAttribute("Notification email ID"),
     SysOperationHelpTextAttribute("Email address to receive the blocked vendor report after batch completion."),
     SysOperationDisplayOrderAttribute('5')]
    public Email parmNotificationEmail(Email _notificationEmail = notificationEmail)
    {
        notificationEmail = _notificationEmail;
        return notificationEmail;
    }

}

Uibuilder class

[DataContractAttribute
, SysOperationContractProcessingAttribute(classStr( my_VendorBlockingUiBuilder))]
Public class  my_VendorBlockingContract
{
    private VendGroupId Vendgroup = "";
    private  myCustomString vendAccount;
    private TransDate fromDate;
    private TransDate toDate;
    private List      VendAccountList; // List of CustAccount strings
    int             inactivityMonths;
    ReasonCode  reasonCode;
    NoYes           applyAllLegalEntities;
    NumberOf            activityPeriod;
    NumberOf            gracePeriod;
    NoYesId             includeEmployees;
    NoYesId             excludeNewVendors;
    Email               notificationEmail;

    [DataMemberAttribute, SysOperationDisplayOrderAttribute('1')]
    public VendGroupId parmVendGroup(VendGroupId _vendGroup = Vendgroup)
    {
        Vendgroup = _vendGroup;
        return Vendgroup;
    }

    [DataMemberAttribute("Vendor Account"), SysOperationLabelAttribute(literalstr("Vendor Account")),SysOperationDisplayOrderAttribute('2')]
    public  myCustomString parmvendAccount( myCustomString _vendAccount = vendAccount)
    {
        vendAccount = _vendAccount;
        return vendAccount;
    }

    [DataMemberAttribute("@SYS300098"), SysOperationLabelAttribute(literalstr("@SYS300098")), SysOperationDisplayOrderAttribute('3')]
    public ReasonCode parmReasonCode(ReasonCode _reasonCode = reasonCode)
    {
        reasonCode = _reasonCode;

        return reasonCode;
    }

    [DataMemberAttribute('ApplyAllLegalEntities'),
     SysOperationLabelAttribute("Apply across all legal entities"),
     SysOperationDisplayOrderAttribute('4')]
    public NoYes parmApplyAllLegalEntities(NoYes _applyAllLegalEntities = applyAllLegalEntities)
    {
        applyAllLegalEntities = _applyAllLegalEntities;
        return applyAllLegalEntities;
    }

    [DataMemberAttribute('NotificationEmail'),
     SysOperationLabelAttribute("Notification email ID"),
     SysOperationHelpTextAttribute("Email address to receive the blocked vendor report after batch completion."),
     SysOperationDisplayOrderAttribute('5')]
    public Email parmNotificationEmail(Email _notificationEmail = notificationEmail)
    {
        notificationEmail = _notificationEmail;
        return notificationEmail;
    }
}



Service class


class  my_VendorBlockingService extends SysOperationServiceBase
{
    ReasonCode          reasonCode;
    NoYes               applyAllLegalEntities;
    NumberOf            activityPeriod;
    NumberOf            gracePeriod;
    NoYesId             includeEmployees;
    NoYesId             excludeNewVendors;
    VendTrans           VendTrans;
    VendTable           _vendTableUpdate, vendtable;
    VendTrans           vendTrans1;
    Guid                runId;
    public void processSVendBlocking( my_VendorBlockingContract contract)
    {
        List            vendAccounts = new List(Types::String);
        ListEnumerator  enumerator;
        VendTable       vendloc;
        TransDate       cutOffDate;

        VendGroupId vendGroup           = contract.parmVendGroup();
         myCustomString vendAccountstr   = contract.parmvendAccount();
        vendAccounts                    = strSplit(vendAccountstr, ";");
        enumerator                      = vendAccounts.getEnumerator();
        runId                           = newGuid();

        cutOffDate                      = dateMthFwd(systemDateGet(), -18);


        
        //TransDateTime latestGraceDate = DateTimeUtil::addDays(DateTimeUtil::utcNow(), - contract.parmGracePeriod());
        //TransDateTime latestPossibleDate = DateTimeUtil::addDays(DateTimeUtil::utcNow(), -contract.parmActivityPeriod());

        if (vendAccountstr && vendAccounts.elements() > 0)
        {
            ListEnumerator le = vendAccounts.getEnumerator();
            while (le.moveNext())
            {
                VendAccount acct = le.current();
                VendTable vend = VendTable::find(acct);
                this.blockInactiveVendor(vend, cutOffDate,contract,runId);
            }
        }
        else if (vendGroup)
        {
            while select AccountNum from vendloc where vendloc.VendGroup == vendGroup
            {
                this.blockInactiveVendor(vendloc, cutOffDate,contract,runId);
            }
        }
        else
        {
            while select AccountNum from vendloc where (vendloc.Blocked != CustVendorBlocked::Never) &&
                    ((vendloc.Blocked == CustVendorBlocked::No) ||
                    (vendloc.BlockedReleaseDate >= DateTimeUtil::utcNow()))
            {
                this.blockInactiveVendor(vendloc, cutOffDate,contract,runId);
            }
        }

         my_VendorBlockingExcelHelper::generateAndSendBlockedVendorReport(runId, contract.parmNotificationEmail());
        
    }

    private void blockInactiveVendor(VendTable _vendTable, TransDate _custoffdate,  my_VendorBlockingContract contract, guid _runid)
    {
        Email emailaccount = contract.parmNotificationEmail();
        if(contract.parmApplyAllLegalEntities())
        {
            while select crosscompany AccountNum, Party from vendTable
                where vendtable.AccountNum == _vendTable.AccountNum &&
                        vendTable.Blocked == CustVendorBlocked::No  
            {
               
                if (this.isVendorInactive(vendTable.AccountNum, _custoffdate))
                {
                    this.updateVendorBlockAndLog(vendTable, contract, _runId);
                }
            }
        }
        else
        {
            while select AccountNum, Party from vendTable
                where vendtable.AccountNum == _vendTable.AccountNum &&
            (vendTable.Blocked != CustVendorBlocked::Never && vendTable.Blocked == CustVendorBlocked::No)
            {
                if (this.isVendorInactive(vendTable.AccountNum, _custoffdate))
                {
                    this.updateVendorBlockAndLog(vendTable, contract, _runId);
                }
            }
        }
    }

    private boolean isVendorInactive(VendAccount _vendAccount, TransDate _cutoffDate)
    {
        vendTrans1.clear();

        select firstOnly crosscompany RecId from vendTrans1
            where vendTrans1.AccountNum == _vendAccount
               && vendTrans1.TransDate >= _cutoffDate;

        return !vendTrans1.RecId;
    }

    private void updateVendorBlockAndLog(VendTable _vendTable,  my_VendorBlockingContract contract, Guid _runId)
    {
        VendOnHoldHistory         vendOnHoldHistory;
         my_VendorBlockingRunLog   runLog;
        TransDate                 lastTransDate;
        
        ttsbegin;
        changecompany(_vendTable.DataAreaId)
        {
            _vendTableUpdate = VendTable::find(_vendTable.AccountNum, true);

            if (_vendTableUpdate.RecId && _vendTableUpdate.Blocked == CustVendorBlocked::No)
            {
                _vendTableUpdate.Blocked = CustVendorBlocked::All;
                _vendTableUpdate.update();

                VendOnHoldHistory::createHistoryRecord(
                    _vendTableUpdate.AccountNum,
                    CustVendorBlocked::All,
                    contract.parmReasonCode());

                runLog.clear();
                runLog.RunId            = _runId;
                runLog.LegalEntity      = _vendTableUpdate.DataAreaId;
                runLog.AccountNum       = _vendTableUpdate.AccountNum;
                runLog.VendName         = _vendTableUpdate.name();
                runLog.VendGroup        = _vendTableUpdate.VendGroup;
                runLog.blockeddate      =   systemDateGet();
                runLog.ReasonCode       = contract.parmReasonCode();
                runLog.insert();
            }
        }

        ttscommit;
    }

}



Controller class

public class  my_VendorBlockingController extends SysOperationServiceController
{
    protected void new()
    {
        // call service class and service class method
        super(classStr( my_VendorBlockingService), methodStr( my_VendorBlockingService,  processSVendBlocking), SysOperationExecutionMode::Synchronous);
    }

    public static  my_VendorBlockingController construct(SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Synchronous)
    {
         my_VendorBlockingController controller;
        controller = new  my_VendorBlockingController();
        controller.parmExecutionMode(_executionMode);
        return controller;
    }

    public static void main(Args _args)
    {

         my_VendorBlockingController controller;
       
        controller =  my_VendorBlockingController::construct();
        controller.parmArgs(_args);
        controller.startOperation();
    }

    public ClassDescription defaultCaption()
    {
        return "Vendor Bloacking batch";
    }

}



Helper class to send blocked vendors list to specified email

using OfficeOpenXml;
Public class  my_VendorBlockingExcelHelper
{
    public static void generateAndSendBlockedVendorReport(Guid _runId, Email _emailId)
    {
         my_VendorBlockingRunLog        runLog;
        System.IO.MemoryStream         memoryStream;
        OfficeOpenXml.ExcelPackage     package;
        OfficeOpenXml.ExcelWorksheet   worksheet;
        OfficeOpenXml.ExcelRange       cell;
        SysMailerMessageBuilder        messageBuilder;
        str                            fileName;
        int                            row = 1;
        boolean                        hasData = false;

        if (!_emailId)
        {
            info("Notification email is blank. Email skipped.");
            return;
        }

        memoryStream = new System.IO.MemoryStream();

        try
        {
            package = new OfficeOpenXml.ExcelPackage(memoryStream);
            worksheet = package.get_Workbook().get_Worksheets().Add("Blocked Vendors");

            // Header row
            cell = worksheet.get_Cells().get_Item(row, 1);
            cell.set_Value("Company");

            cell = worksheet.get_Cells().get_Item(row, 2);
            cell.set_Value("Vendor Account");

            cell = worksheet.get_Cells().get_Item(row, 3);
            cell.set_Value("Vendor Name");

            cell = worksheet.get_Cells().get_Item(row, 4);
            cell.set_Value("Vendor Group");

            cell = worksheet.get_Cells().get_Item(row, 5);
            cell.set_Value("Blocked Date Time");

            cell = worksheet.get_Cells().get_Item(row, 6);
            cell.set_Value("Reason Code");

            // Data rows
            while select runLog
                where runLog.RunId == _runId
            {
                row++;
                hasData = true;

                cell = worksheet.get_Cells().get_Item(row, 1);
                cell.set_Value(runLog.LegalEntity);

                cell = worksheet.get_Cells().get_Item(row, 2);
                cell.set_Value(runLog.AccountNum);

                cell = worksheet.get_Cells().get_Item(row, 3);
                cell.set_Value(runLog.VendName);

                cell = worksheet.get_Cells().get_Item(row, 4);
                cell.set_Value(runLog.VendGroup);

                cell = worksheet.get_Cells().get_Item(row, 5);
                cell.set_Value(date2StrXpp(runLog.blockeddate));

                cell = worksheet.get_Cells().get_Item(row, 6);
                cell.set_Value(runLog.ReasonCode);
            }

            if (!hasData)
            {
                info("No blocked vendors found for this run. Email skipped.");
                return;
            }

            package.Save();
            memoryStream.Seek(0, System.IO.SeekOrigin::Begin);

            fileName = strFmt("BlockedVendorReport_%1.xlsx", any2Str(systemDateGet()));

            messageBuilder = new SysMailerMessageBuilder();
            messageBuilder.addTo(_emailId);
            messageBuilder.setSubject("Blocked vendor report");
            messageBuilder.setBody("Please find attached blocked vendor report generated after batch completion.");
            messageBuilder.addAttachment(
                memoryStream,
                fileName,
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");

            SysMailerFactory::sendNonInteractive(messageBuilder.getMessage());

            info(strFmt("Blocked vendor report sent to %1", _emailId));
        }
        catch (Exception::CLRError)
        {
            error(CLRInterop::getLastException().ToString());
        }
        finally
        {
            if (package)
            {
                package.Dispose();
            }

            if (memoryStream)
            {
                memoryStream.Dispose();
            }
        }
    }

}

Post-Development Validation

After completion of the development activities, the following validations must be performed before handing over the solution for UAT:

  • Verify SMTP configuration in the target environment.
  • Validate email authentication and ensure email notifications are sent successfully.
  • Configure the notification email IDs in the parameter setup.
  • Execute the Inactive Vendor Blocking batch job using the configured parameters.
  • Verify that vendors satisfying the inactivity criteria are automatically updated with Vendor Blocking = All.
  • Verify that the blocked vendor Excel report is generated successfully.
  • Validate that the generated Excel report is attached to the notification email.
  • Confirm that the notification email is successfully delivered to the configured recipients.
  • Review the batch execution history and logs to ensure the process completes without errors.
  • Perform end-to-end validation of the complete inactive vendor blocking process across the configured legal entities.

This section clearly documents the activities that need to be completed after development to verify the solution is functioning as expected.

Comments