PSA - My solution ended up breaking in the uplift from 10.2.500 to 10.2.700… Not 100% sure of the exact etiology, but it had something to do with the QSalesRP EpiViewNotification in the Quote Entry Customization. I’ve always found EpiViewNotifications a little finicky but I wasn’t able to figure out exactly what changed from one version of Epicor to the other.
Instead, seeing the writing on the wall regarding the future of the WinForms client, I took the opportunity to move code out of Customization and into BPMs. In hindsight I should have done this in the first place, but I’ve only recently gotten the hang of how to do something like this the “correct way” in BPMs (I think)…
Anyways, here is my new solution for Quote, which seems to work well in 10.2.700.4. It is a system of 3 BPMs shown below. There is still a tiny bit in Customization that I won’t bother to show; it just does an oTrans.Refresh to make the UX a little smoother.
Step 1 - Post-Processing BPM on Quote.GetNewQuoteHed:
/*
Quote.GetNewQuoteHed | Post-Processing | RBx2_SetPrimarySalesRep_Quote
When new Quotes are created, the Primary Salesperson field is set to the SalesRep linked to the current user.
User is linked to SalesRep by way of PerCon. If no linkage is found, exception is raised preventing Quote creation.
*/
foreach (var addedQuoteHedRow in ttQuoteHed.Where(qh_a => qh_a.Added()))
{
string errorMessage = "You must be configured as a Salesperson to create new Quotes. Please contact your system administrator.";
int linkedPerConID = Db.PerCon.Where(percon =>
percon.Company == addedQuoteHedRow.Company
&& percon.DcdUserID == callContextClient.CurrentUserId
).Select(percon => percon.PerConID).FirstOrDefault();
if (linkedPerConID == 0)
{
throw new Ice.BLException(errorMessage);
}
string linkedSalesRepCode = Db.SalesRep.Where(salesrep =>
salesrep.Company == addedQuoteHedRow.Company
&& salesrep.PerConID == linkedPerConID
).Select(salesrep => salesrep.SalesRepCode).FirstOrDefault();
if (linkedSalesRepCode == null)
{
throw new Ice.BLException(errorMessage);
}
addedQuoteHedRow.SalesRepCode = linkedSalesRepCode;
}
Step 2 - Pre-Processing BPM on Quote.Update:
/*
Quote.Update | Pre-Processing | RBx2_QuoteSalesRepCleanupScenarios_PRE
There are several scenarios where Epicor Business Logic causes unwanted Quote-Salesperson behaviors.
Here, we flag such occurrences by writing the pre-updated ttQuoteHed.SalesRepCode (the current Primary Salesperson) to callContextBpmData.Character02.
A post-processing BPM fires when callContextBpmData.Character02 != "", and counteracts the unwanted behavior.
Note - We do not attach Salespersons to Customers or ShipTos, but a Primary Salesperson is linked to each Sales Territory (due to system requirement).
*/
// 1. Upon the first save of a new Quote, Epicor tries to add an unwanted second QSalesRP record for the Customer's Territory's Primary Salesperson.
foreach (var added_QuoteHedRow in ttQuoteHed.Where(qh_a => qh_a.Added()))
{
this.callContextBpmData.Character02 = added_QuoteHedRow.SalesRepCode;
}
foreach (var updated_QuoteHedRow in ttQuoteHed.Where(qh_u => qh_u.Updated()))
{
string updated_CustomerCustID = updated_QuoteHedRow.CustomerCustID;
string updated_ShipToCustID = updated_QuoteHedRow.ShipToCustID;
string updated_ShipToNum = updated_QuoteHedRow.ShipToNum;
string updated_SalesRepCode = updated_QuoteHedRow.SalesRepCode;
var original_QuoteHedRowData = ttQuoteHed.Where(qh_o =>
qh_o.Unchanged()
&& qh_o.Company == updated_QuoteHedRow.Company
&& qh_o.SysRowID == updated_QuoteHedRow.SysRowID)
.Select(qh_o => new {
CustomerCustID = qh_o.CustomerCustID,
ShipToCustID = qh_o.ShipToCustID,
ShipToNum = qh_o.ShipToNum,
SalesRepCode = qh_o.SalesRepCode})
.FirstOrDefault();
if (updated_CustomerCustID != original_QuoteHedRowData.CustomerCustID
|| updated_ShipToCustID != original_QuoteHedRowData.ShipToCustID
|| updated_ShipToNum != original_QuoteHedRowData.ShipToNum)
{
this.callContextBpmData.Character02 = original_QuoteHedRowData.SalesRepCode;
// need to exit so below code doesn't fire for scenario where SR changes before Save
// (happens after tabbing off when there is a Salesperson linked at Customer or ShipTo level)
return;
}
// if Primary Salesperson is changed manually, we still need to make sure RepSplit gets set in POST BPM
if (updated_SalesRepCode != original_QuoteHedRowData.SalesRepCode)
{
this.callContextBpmData.Character02 = updated_SalesRepCode;
}
}
Step 3 - Post-Processing BPM on Quote.Update:
/*
Quote.Update | Post-Processing | RBx2_QuoteSalesRepCleanupScenarios_POST
*/
if (this.callContextBpmData.Character02 != "")
{
string primarySalesRepCode = this.callContextBpmData.Character02;
this.callContextBpmData.Character02 = "";
int quoteNum = ttQuoteHed.Select(qh => qh.QuoteNum).FirstOrDefault();
var quoteSVC = Ice.Assemblies.ServiceRenderer.GetService<Erp.Contracts.QuoteSvcContract>(this.Db);
using (quoteSVC)
{
QuoteTableset quoteDS = quoteSVC.GetByID(quoteNum);
foreach (var quoteSalesRepRow in quoteDS.QSalesRP.Where(qsr => !qsr.PrimeRep))
{
quoteSalesRepRow.RowMod = "D";
}
foreach (var quoteSalesRepRow in quoteDS.QSalesRP.Where(qsr => qsr.PrimeRep))
{
if (quoteSalesRepRow.SalesRepCode != primarySalesRepCode)
{
quoteSalesRepRow.SalesRepCode = primarySalesRepCode;
}
quoteSalesRepRow.RepSplit = 100;
quoteSalesRepRow.RowMod = "U";
}
quoteSVC.Update(ref quoteDS);
this.dsHolder.Attach(quoteDS);
// trigger another refresh in Quote customization -- UI still shows 2 salespersons in some scenarios until refresh is clicked.
this.callContextBpmData.Checkbox07 = true;
}
}