How to Prevent Users from Creating Empty POs in Dynamics 365 Finance and Supply Chain

By Deovandski Skibinski | July 31, 2025

If you came across this blog, then you might have already tried a few things to prevent users from creating POs with empty lines in Dynamics 365 Finance and Supply Chain. Given we cannot execute a CoC on canClose() method on PurchTable form, and placing a validateWrite() on datasource doesn't trigger validation if the user does not make any edits to a given empty PO, then it leaves us with few choices that aren't super intrusive.

This code example for D365 F&O below will not force the user to act when seeing a PO with empty line, or prevent the user from leaving the form, but it will instead prompt the user to delete the PO. If they choose Yes, the PO is deleted, and the user returns to the PO grid without issues.

This code is the least intrusive way to perform a check and nudge the user to act when they click on a PO in the PO list, then press back without making any changes.

[FormControlEventHandler(formControlStr(PurchTable, TabPageDetails), FormControlEventType::AllowPageDeactivate)]
public static void TabPageDetails_AllowPageDeactivate(FormControl sender, FormControlEventArgs e)
{
 
    // Cast sender to FormTabControl
    FormTabControl tabControl = sender as FormTabControl;

    // Use known tab page control names to check visibility
    FormRun formRun = sender.formRun();
    FormControl tabPageDetails = formRun.design().controlName(formControlStr(PurchTable, TabPageDetails));

    if (tabPageDetails.visible())
    {
        FormDataSource purchLineDS = formRun.dataSource(formDataSourceStr(PurchTable, PurchLine));
        PurchLine purchLineRecord = purchLineDS.getFirst();

        if (!purchLineRecord)
        {
            // Get the PurchTable datasource
            FormDataSource purchTableDS = formRun.dataSource(formDataSourceStr(PurchTable, PurchTable));
       
            PurchTable purchTable = purchTableDS.cursor() as PurchTable;
           
            // Show a confirmation dialog
            if (Box::yesNo("This PO has no lines. Delete it?", DialogButton::No) == DialogButton::Yes)
            {
                ttsBegin;
                purchTable.delete();
                ttsCommit;

                // Refresh the datasource to reflect the deletion
                purchTableDS.research(true);
            }

        }
    }
}// Another possibility is to hook into tab change event.
  [FormControlEventHandler(
      formControlStr(PurchTable, MainTab),
      FormControlEventType::TabChanged)]
  public static void MainTab_OnTabChanged(FormControl sender, FormControlEventArgs e)
  {
      FormRun formRun = sender ? sender.formRun() : null;
      if (!formRun)
          return;

      // Optional: if the form was opened via deep link (no caller), do nothing.
      // TabChanged normally doesn't fire on initial load, but this avoids edge cases.
      Args a = formRun.args();
      // if (!a || !a.caller()) return;

      FormTabControl tab = sender as FormTabControl;
      FormTabControlTabChangedEventArgs tce = e as FormTabControlTabChangedEventArgs;
      if (!tab || !tce)
          return;
     
      // Resolve the Details page control
      FormTabPageControl detailsPage =
          formRun.design().controlName(formControlStr(PurchTable, TabPageDetails)) as FormTabPageControl;
      if (!detailsPage)
          return;
     
      // oldTab/newTab are INDEXES, not control IDs
      int oldIdx = tce.oldTab();

      // Map index -> page control
      FormTabPageControl oldPage = tab.controlNum(oldIdx) as FormTabPageControl;

      // Only when LEAVING the Details page back to the grid view
      if (oldPage && oldPage.id() == detailsPage.id())
      {
          FormDataSource purchTableDS = formRun.dataSource(formDataSourceStr(PurchTable, PurchTable));
          FormDataSource purchLineDS  = formRun.dataSource(formDataSourceStr(PurchTable, PurchLine));

          PurchTable purchTable = purchTableDS ? (purchTableDS.cursor() as PurchTable) : null;
          PurchLine  lineFirst  = purchLineDS ? purchLineDS.getFirst() : null;

          if (purchTable && !lineFirst && !PurchLine::exist(purchTable.PurchId))
          {
         if (Box::yesNo("This PO has no lines. Delete it?", DialogButton::No) == DialogButton::Yes)
              {
                  ttsBegin;
                  purchTable.delete();
                  ttsCommit;

                  purchTableDS.research(true);
              }
          }
      }
  }
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!