I wrote a library that connects an external software with Epicor that processes POs and Invoices. The function takes a long string and separates the values on a specific basis, giving me all the properties needed to create an Invoice.
I’m having an issue with Error Handling on the AP Detail level. The program works as expected in every aspect except when trying to push an error through the system on the detail. In this test, we tried to send a receipt line and PO that doesn’t exist in the system. I was expecting the detail to error out, propagate the error to the caller which is also surrounded by a catch block. In this catch, I exit the loop, handle the error by notating the invoice head that errored, and then removing the details that may have been posted to the invoice head previously as the missing line would cause a variance.
When sending bad data to the head, the program errors properly and sends back an error to the caller which is handled like the detail should, it stores the error and moves on to the next head. Do I have my try/catch blocks misplaced? Am I missing something with the error handling?
‘Main’ Function
string errorLines = "";
string grpID = "";
int hedCount = 0;
int dtlCount = 0;
string dtlReturn = "";
if (!string.IsNullOrEmpty(VisionData))
{
try
{
.....
//
// Create APInvGroups for Invoices to be held in
//
grpID = this.ThisLib.CreateAPInvGroup();
//
// Create multidimensional list to hold the values of each invoice head
// Dim.1 = Invoice Head ------ Dim.2 = Invoice Detail information
//
List<List<string>> InvoiceList = new List<List<string>>();
....... // Create multidimensional list using loops to verify if it's been added already or not. If so, it adds another list item to the already populated head, else it adds a new invoice item to the list
//
// Loop through each head (first dimension of list)
//
bool hedCreated = false;
foreach (var line in InvoiceList)
{
try
{
//
// Create array of fields to populate APDtl & APHed
//
var fields = line[0].Split('|');
hedCreated = false;
//
// Verfies that the head hasn't been created already
// resulting in an error being sent and a failure occurring
// on the invoice as a whole
//
if (!hedCreated)
{
//
// *Create New Head
// *Toggle bool that holds the value
// whether the hed has been created
// *Increment the hed count for the
// multidimensional list
//
this.ThisLib.CreateNewHed(line[0], grpID);
hedCreated = true;
hedCount++;
}
//
// Set the detail counter to 0 and loop through
// each line and add the detail into Epicor.
//
dtlCount = 0;
foreach(var apDtl in line)
{
//
// Create New Dtl
//
this.ThisLib.CreateNewDtl(apDtl, grpID); // Doesn't error properly here
//
// Increment the counter for use in an error
//
dtlCount++;
}
} catch(Exception ex)
{
//
// Add errored line to listing
//
if(string.IsNullOrEmpty(errorLines))
{
//
// if the string is empty, we don't want a tilde
// at the beginning of the string causing issues
// in the Console App outside of Epicor.
//
errorLines = $"{line[0]}|{ex.InnerException}";
} else
{
//
// If the string isn't blank, we want to append the
// new errored line and tag on the error description
// on the end of the line.
//
errorLines = $"{errorLines}~{line[0]}|{ex.InnerException}";
}
if (hedCreated)
{
//
// Details need deleted prior to the head deletion
//
if (dtlCount > 0)
{
//
// If the detail count if 0, we skip looping
// but we have already determined that the
// head was created thus needs deleted.
//
// If the count is greater than 0, we will
// loop through each of the details and
// send the lines to the deletion method
// for handling
//
for(int i = 0; i < dtlCount; i++)
{
//
// Send the section defined to the deletion function
//
this.ThisLib.DeleteAPDetail(InvoiceList[hedCount][i], grpID);
}
}
//
// Send the first item on list to get Invoice information to
// function for deletion
//
this.ThisLib.DeleteAPHead(InvoiceList[hedCount][0], grpID);
}
}
}
if (InvoiceList.Count == 0)
{
this.ThisLib.DeleteAPGroup(grpID);
grpID = "";
}
} catch (Exception ex)
{
this.ThisLib.DeleteAPGroup(grpID);
EpiResponse = false;
ErroredLines = VisionData;
GroupID = "";
}
}
‘CreateNewHed’ Function that works and returns errors as intended
try
{
..... // Set a bunch of 'out' variables
using(var trx = IceContext.CreateDefaultTransactionScope())
{
//
// Build context and get services from Epicor to perform
// database operations and set PO Variable
//
var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
var APInv = ServiceRenderer.GetService<Erp.Contracts.APInvoiceSvcContract>(context);
int PONum = fields[24] == "" ? 0 : Convert.ToInt32(fields[24].Trim());
//
// Follow trace performed within Epicor to verify the
// methods utilized to create Heads
//
APInv.GetNewAPInvHedInvoice(ref ts, GroupID);
//
// Add the proper company to the invoice
//
ts.APInvHed[0].Company = fields[0].Trim();
APInv.ChangeRefPONum(PONum, true, out confMess, ref ts);
APInv.ValidateInvoiceID(fields[1], fields[3], out vendorNumOut, out APInvFound);
APInv.ChangeInvoiceDateWithDateCheck(new DateTime(Convert.ToInt32(fields[15].Substring(0, 4)), Convert.ToInt32(fields[15].Substring(4, 2)), Convert.ToInt32(fields[15].Substring(6, 2))), "", out messText, out DateMess, ref ts);
APInv.PreUpdate(ref ts, out userInput);
APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
//
// Add values to specific fields on the Head
//
ts.APInvHed[0].InvoiceNum = fields[3].Trim();
ts.APInvHed[0].DueDate = new DateTime(Convert.ToInt32(fields[15].Substring(0, 4)), Convert.ToInt32(fields[15].Substring(4, 2)), Convert.ToInt32(fields[15].Substring(6, 2)));
ts.APInvHed[0].InvoiceDate = new DateTime(Convert.ToInt32(fields[4].Substring(0, 4)), Convert.ToInt32(fields[4].Substring(4, 2)), Convert.ToInt32(fields[4].Substring(6, 2)));
ts.APInvHed[0].RateGrpCode = "MAIN";
ts.APInvHed[0].ScrDocInvoiceVendorAmt = fields[18].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[18].Trim());
//
// In the instance the record is a NON-PO, this will
// include the required fields on the new row
//
if (PONum == 0)
{
ts.APInvHed[0].VendorNum = vendorNumOut;
ts.APInvHed[0].TermsCode = "N30";
ts.APInvHed[0].TaxAmt = fields[22].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[22].Trim());
}
//
// Final update
//
APInv.UpdateMaster(ref ts, GroupID, "APInvHed", true, true, true, true, out grpTotalInvAmt, out reqUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out DUAMsg);
//
// Close and dispose of the Transaction
// Scope for the next use
//
trx.Complete();
trx.Dispose();
APInv.Dispose();
}
} catch (Exception ex)
{
throw new Exception("There was an error with the Head", ex);
}
‘CreateNewDtl’ Function doesn’t return properly
var fields = Line.Split('|');
try
{
//
// 'out' variables used within Epicor space
//
string errMess;
string opLocMsg;
decimal grpTotalAmt;
bool requireUserInput;
string opMess;
string opMsgChk;
string opChkRev;
bool GenLedgNum;
bool UpdateRan;
string opDUAMsg;
string glAcctDisp;
string glAcctDesc;
decimal grpTotalInvAmt;
string comp = fields[0].Trim();
string ID = fields[1];
//
// Required Variables
//
var RcptBill = new Erp.Tablesets.APInvReceiptBillingTableset();
var APInvTS = new Erp.Tablesets.APInvoiceTableset();
var Vend_Row = this.Db.Vendor.Where(vend => vend.Company == comp && vend.VendorID == ID).FirstOrDefault();
if (Vend_Row != null)
{
//
// Verify that the Vendor Row does have data prior to performing actions
//
using (var trx = IceContext.CreateDefaultTransactionScope())
{
//
// Set the vendor number and PO Number for use in multiple
// fields. Run once and store the value
//
int VendNum = Vend_Row.VendorNum;
int PONum = fields[24] == "" ? 0 : Convert.ToInt32(fields[24].Trim());
//
// Build context and get services from Epicor to perform
// database operations
//
var context = Ice.Services.ContextFactory.CreateContext<ErpContext>();
var APInv = ServiceRenderer.GetService<Erp.Contracts.APInvoiceSvcContract>(context);
var GLAcc = ServiceRenderer.GetService<Erp.Contracts.GLAccountSvcContract>(context);
//
// Get Invoice Details
//
APInv.GetByID(VendNum, fields[3].Trim());
//
// Determine if there is a PO Number attached
// to the invoice, if so we can grab details from
// the PO, else we need to populate the fields
// with Vision Data provided
//
if (PONum != 0)
{
//
// Gets all uninvoiced receipts against the PONumber
// and the corresponding receipt lines
//
APInv.GetAPUninvoicedReceipts(ref RcptBill, VendNum, fields[3].Trim(), PONum);
APInv.GetAPUninvoicedReceiptLines(ref RcptBill, VendNum, "", fields[28].Trim(), false, fields[3].Trim(), PONum); // Believed to be error line; not caught in Exception - FATAL error if failed
//
// Set the proper Receipt Line = true
// and trigger the row mod to 'U' so
// Epicor knows the value has changed
//
foreach(var prt in RcptBill.APUninvoicedRcptLines)
{
if(prt.PackSlip == fields[28].Trim() && prt.PackLine == Convert.ToInt32(fields[29].Trim()))
{
prt.SelectLine = true;
prt.RowMod = "U";
}
}
//
// attached the selected Receipt Line to the Invoice Detail
//
APInv.SelectUninvoicedRcptLines(ref RcptBill, VendNum, "", PONum, fields[28].Trim(), false, fields[3].Trim(), false);
//
// Grabs the rest of the Detail data. Think
// assigned G/L Code, Receipt information, cost, etc.
//
APInv.InvokeInvoiceSelectedLines(ref RcptBill, out opLocMsg);
} else
{
//
// Get new Misc Dtl object with defaulted fields
// then call the ChangePartNum method to bring in details about
// the part if it was within the part table
//
APInv.GetNewAPInvDtlMiscellaneous(ref APInvTS, VendNum, fields[3].Trim());
APInv.ChangePartNum("Vision NON-PO Part", ref APInvTS);
//
// Set the description of the part, if nod esscription provided, set
// the value to Vision Part.
//
APInvTS.APInvDtl[0].Description = fields[33].Trim() == "" ? "Vision Part" : fields[33].Trim();
//
// Change the quantity and cost with Epicor methods to verify data
// and create other objects such as GL Acct and Tax information
//
APInv.ChangeVendorQty(Convert.ToDecimal(fields[30].Trim()), ref APInvTS);
APInv.ChangeUnitCost((fields[31].Trim() == "" ? 0.0m : Convert.ToDecimal(fields[31])), ref APInvTS);
}
//
// Verify the Vendor and make the final save
//
APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
APInv.UpdateMaster(ref APInvTS, GroupID, "APInvDtl", true, false, true, false, out grpTotalAmt, out requireUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out opDUAMsg);
//
// Update GL code again due to data-saving needing to taking place
// prior to changing G/L code. Once the data is in DB, we can
// alter the data in the table and resubmit
//
if (PONum == 0)
{
//
// Update the APInvExp table with the correct G/L code and toggle RowMod = U
// for updating
//
APInvTS.APInvExp[0].GLAccount = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
APInvTS.APInvExp[0].ExpDispGLAcct = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
APInvTS.APInvExp[0].RowMod = "U";
//
// Perform the same actions on the APInvExpTGLC table
//
APInvTS.APInvExpTGLC[0].GLAccount = $"{fields[9].Trim()}|00|{fields[10].Trim()}";
APInvTS.APInvExpTGLC[0].SegValue1 = fields[9].Trim();
APInvTS.APInvExpTGLC[0].SegValue2 = "00";
APInvTS.APInvExpTGLC[0].SegValue3 = fields[10].Trim();
APInvTS.APInvExpTGLC[0].RowMod = "U";
//
// Validate the G/L Account is active and current
//
GLAcc.ValidateGLAccount("MAIN", $"{fields[9].Trim()}|00|{fields[10].Trim()}", "", "", null, true);
GLAcc.GetGLAcctDispAndDesc("MAIN", $"{fields[9].Trim()}|00|{fields[10].Trim()}", "", false, "", out glAcctDisp, out glAcctDesc);
//
// Make a save against the APInvExp table
//
APInv.CheckVendorTaxID(fields[1].Trim(), out errMess);
APInv.UpdateMaster(ref APInvTS, GroupID, "APInvExp", false, false, false, true, out grpTotalAmt, out requireUserInput, out opMess, out opMsgChk, out opChkRev, out GenLedgNum, out UpdateRan, out opDUAMsg);
}
//
// Complete and close the transaction for
// the next
//
trx.Complete();
trx.Dispose();
APInv.Dispose();
GLAcc.Dispose();
} // End of using statement
} // end of Vend_Row If-statement
} catch (Exception ex)
{
TestString = ex.Message;
throw new Exception("There was an error building the Detail", ex);
}
I believe the issue lands on the GetAPUninvoicedReceiptLines as I threw a catch around it and returned the error (which worked properly) saying that it was an invalid receipt. I would expect that error to be passed through, but it isn’t for some reason. I’ve been testing using the Swagger page and Schedule Function module in Epicor. Any help would be super appreciated! I’ve spent more time than I’d like to admit running through this trying to identify the error and it’s for a SOX compliance action at our company.