Epicor Customization: Allow PO Generation for Sales Orders on Credit Hold
by: Doug Jacobson
Here is an interesting business requirement that we feel might not be all that uncommon.
Our client's business process dictates that nearly all customers be on credit hold, forcing the intentional release of credit hold for many business operations.
Our client has two companies in Epicor. Let's call one SupplyCo and the other CustomerCo.
CustomerCo has Buy To Order sales orders which they purchase from SupplyCo.
CustomerCo has a policy to keep nearly all its customers on credit hold, which means nearly all sales orders are also on credit hold.
In order place a PO with SupplyCo using Purchase Order Suggestion Entry > Generate Purchase Orders, any Buy To Order sales orders have to first be taken off credit hold.
CustomerCo's manual process is to take an order (or orders) off credit hold, Generate Purchase Orders from Purchase Order Suggestion Entry, and then put the order(s) back on credit hold .
Recently, they asked if we could automate this process for them.
In order to solve this requirement, we traced the Purchase Order Enty action "Generate Purchase Orders…".
This showed us that the Method Directive involved is Erp.BO.POSugg.Generate*.
Armed with this knowledge, we created a new Pre-Processing directive with the plan to take "Buy Direct" sales orders to our special supplier off credit hold.
After we generated our purchase orders, we planned to put those orders back on credit hold via a new Post-Processing directive.
Our trace of Erp.BO.POSugg.Generate showed a dataset parameter named POSuggDataSet.
This dataset contained our PO Suggestion, which is a SugPoDtl.
The data looks like this:
<parameter name="ds" type="Erp.BO.POSuggDataSet">
<POSuggDataSet xmlns="http://www.epicor.com/Ice/300/BO/POSugg/POSugg">
<SugPoDtl>
<Company>CustomerCo</Company>
<SugNum>546987</SugNum>
… most data omitted, customer data obfuscated…
<OrderNum>123456</OrderNum>
<SoldToNum>4567</SoldToNum>
<VendorID>SupplyCo</VendorID>
<Buy>true</Buy>
</SugPoDtl>
</POSuggDataSet>
</parameter>
So far so good.
Our next step was to take order number 123456 off credit hold.
The Customer Credit Manager screen allows you to do this, so we did another trace while we took order 123456 off credit hold.
Our trace showed three Method Directives were called in the Erp.Proxy.BO.CreditManagerImpl business object.
These are GetOrders, ChangeOrderCreditHold, and UpdateCMOrderHed.
We wrote code that used the business objects to take orders off credit hold. Below is a portion of that code:
****** Begin Code Excerpt ******
using(var credMgrSvc = Ice.Assemblies.ServiceRenderer.GetService<Erp.Contracts.CreditManagerSvcContract>(Db))
{
var cmOrderHedDS = credMgrSvc.GetOrders(cust.CustID);
foreach(var cmOrdHedRow in cmOrderHedDS.CMOrderHed)
{
if(cmOrdHedRow.OrderNum == order.OrderNum)
{
cmOrdHedRow.CreditHold = false;
cmOrdHedRow.CreditOverride = true;
cmOrdHedRow.CreditOverrideLimit = creditHoldValue ? order.TotalCharges : 0;
cmOrdHedRow.RowMod = "U";
updateCredit = true;
updatedOrders.Add(order.OrderNum.ToString());
}
}
if(updateCredit)
{
credMgrSvc.UpdateCMOrderHed(cust.CustNum, ref cmOrderHedDS);
credMgrSvc.ChangeOrderCreditHold(ref cmOrderHedDS);
}
****** End Code Excerpt ******
We tested this code on it's own and it worked perfectly.
Next, we put this code in our Erp.BO.POSugg.Generate.Pre-Processing directive and tested it.
It didn't work. Nothing happened. Orders in the trace stayed on credit hold.
Troubleshooting revealed that the dataset didn't contain any records. What madness was this?
A bit of searching came up with this AI provided result:
"This is a known issue in Epicor ERP (specifically E10/Kinetic) when working with the Erp.BO.POSugg.Generate method. The trace shows data because the UI sends the full suggestion record to the server, but the Generate method often initializes with an empty ds.SugPoDtl dataset in pre-processing before it populates it internally."
Undaunted we forged ahead. We'd need another way to accomplish our task.
Since Purchase Order Suggestion Entry works with PoSugDtl records, we decided to build our own list of records.
We only look at PoSugDtl's where the Company is CustomerCo, the supplier is SupplyCo, and Buy is true.
Next we'd use our CreditManager code to take these order off hold.
It didn't work. CreditManager wasn't able to take orders off credit hold when called from Erp.BO.POSugg.Generate.Pre-Processing.
A little daunted but still determined, we trudged on.
Running out of options, we pivoted to an approach of last resort. Direct Database updates. We like to avoid these unless we know we're updating something thay may have donwstream dependencies, such as UD fields - but sometimes it's your only choice and since we're going to put these orders right back on credit hold we moved forward.
Let's look at the code:
First we select the records to process
****** Begin Code Excerpt ****** var buyToOrderSugPoDtls = Db.SugPoDtl.Where(x=> x.Company == "CustomerCo" && x.VendorID == "SupplyCo" && x.Buy); ****** End Code Excerpt ******
We then loop through these records and check to see if the customer (from PoSugDtl.SoldToNum) is on credit hold. If not, we don't need to do anything. Since most will be on credit hold, we'll take the order off credit hold. We also need to keep track of the orders we've taken off credit hold to send to post-processing where we put them back on credit hold. Note that the field is actually OrderHed.CreditOverride.
****** Code Continued ******
//create an empty list to track orders we take off credit hold
var updatedOrders = new List<string>();
//check each updated buy direct order. Remember, we've limited our records to VendorID="SupplyCo"
foreach(var sugPoDtl in buyToOrderSugPoDtls)
{
var cust = Db.Customer.FirstOrDefault(x=>x.Company == CompanyID && x.CustNum == sugPoDtl.SoldToNum);
//cust shouldn't be null, but we like to to null tests in our code. Nulls that slip through cause issues!
if(cust != null)
{
if(cust.CreditHold)
{
//The customer for the order is on credit hold, let's take the order off credit hold
var order = Db.OrderHed.FirstOrDefault(x=>x.Company == CompanyID && x.OrderNum == sugPoDtl.OrderNum);
//another null test, can't be too safe!
if(order != null)
{
order.CreditOverride = true;
order.CreditOverrideLimit = order.TotalCharges;
//Add the order number to our list
updatedOrders.Add(order.OrderNum.ToString());
}
}
}
}
if(updatedOrders.Count() > 0)
{
//Convert the updatedOrders list to a tilde separated string. We'll use this in post-processing to put these orders back on hold
callContextBpmData.Character01 = string.Join("~",updatedOrders);
EnablePostProcessing();
}
****** End Code******
Next we built our post-processing directive. It converts callContextBpmData.Character01 back into a list of orders. It loops through the list of orders and puts them back on credit hold.
*Note that this customer still uses the smart client. In the Kinetic UI, the method called is Erp.BO.POSugg.ShowGenerated