How to Prevent Users from Creating Empty POs in Dynamics 365 Finance and Supply Chain
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);
}
}
}
}
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.

