@alintz – Thank you very much for sharing!
At first I balked at the idea of doing the heavy-lifting in a customization (just didn’t feel right to me), but after trying an absurd number of other approaches, I ended up doing something very similar for Quotes! It still feels a tad inelegant, but hey, it works, it’s performant, and I get to avoid doing any freaking BPM database updates…
Here’s my solution (for version 10.2.500.6). You’ll see from the code that our requirements were a bit different than yours, but it’s just a variation on the same theme:
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_SalesRepCleanupScenarios_Quote
We currently know of 3 scenarios where Epicor Business Logic causes unwanted Quote-Salesperson behaviors.
Here, we flag such occurrences by writing ttQuoteHed.SalesRepCode (the current Primary Salesperson) to callContextBpmData.Character01.
Quote Entry customization code fires when callContextBpmData.Character01 != "", 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 addedQuoteHedRow in ttQuoteHed.Where(qh_a => qh_a.Added()))
{
this.callContextBpmData.Character01 = addedQuoteHedRow.SalesRepCode;
}
foreach (var updatedQuoteHedRow in ttQuoteHed.Where(qh_u => qh_u.Updated()))
{
decimal updatedCustNum = updatedQuoteHedRow.CustNum;
decimal originalCustNum = ttQuoteHed.Where(qh_o =>
qh_o.Unchanged()
&& qh_o.Company == updatedQuoteHedRow.Company
&& qh_o.SysRowID == updatedQuoteHedRow.SysRowID
).Select(qh_o => qh_o.CustNum).FirstOrDefault();
// 2. If the Customer is changed, Epicor tries to replace the current Primary Salesperson with the Customer's Territory's Primary Salesperson.
if (updatedCustNum != originalCustNum)
{
this.callContextBpmData.Character01 = updatedQuoteHedRow.SalesRepCode;
return;
}
string updatedShipToNum = updatedQuoteHedRow.ShipToNum;
string originalShipToNum = ttQuoteHed.Where(qh_o =>
qh_o.Unchanged()
&& qh_o.Company == updatedQuoteHedRow.Company
&& qh_o.SysRowID == updatedQuoteHedRow.SysRowID
).Select(qh_o => qh_o.ShipToNum).FirstOrDefault();
// 3. If the ShipToNum is changed, Epicor tries to add an unwanted second QSalesRP record for the Customer's ShipTo's Territory's Primary Salesperson.
if (updatedShipToNum != originalShipToNum)
{
this.callContextBpmData.Character01 = updatedQuoteHedRow.SalesRepCode;
return;
}
}
Step 3 - Quote Entry Customization (just the relevant parts to this problem):
using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;
using System.Collections;
using System.Drawing;
using Erp.Adapters;
using Erp.UI;
using Erp.BO;
using Ice.Lib;
using Ice.Adapters;
using Ice.BO;
using Ice.Lib.Customization;
using Ice.Lib.ExtendedProps;
using Ice.Lib.Framework;
using Ice.Lib.Searches;
using Ice.UI.FormFunctions;
using Infragistics.Win;
public class Script
{
private EpiBaseAdapter oTrans_adapter;
public bool triggerSalesRepCleanup = false;
private EpiDataView edvQSalesRP;
public void InitializeCustomCode()
{
this.oTrans_adapter = ((EpiBaseAdapter)(this.csm.TransAdaptersHT["oTrans_adapter"]));
this.oTrans_adapter.AfterAdapterMethod += new AfterAdapterMethod(this.oTrans_adapter_AfterAdapterMethod);
this.edvQSalesRP = ((EpiDataView)(this.oTrans.EpiDataViews["QSalesRP"]));
this.edvQSalesRP.EpiViewNotification += new EpiViewNotification(this.edvQSalesRP_EpiViewNotification);
this.QSalesRP_Column.ColumnChanged += new DataColumnChangeEventHandler(this.QSalesRP_AfterFieldChange);
}
public void DestroyCustomCode()
{
this.oTrans_adapter.AfterAdapterMethod -= new AfterAdapterMethod(this.oTrans_adapter_AfterAdapterMethod);
this.oTrans_adapter = null;
this.edvQSalesRP.EpiViewNotification -= new EpiViewNotification(this.edvQSalesRP_EpiViewNotification);
this.edvQSalesRP = null;
this.QSalesRP_Column.ColumnChanged -= new DataColumnChangeEventHandler(this.QSalesRP_AfterFieldChange);
}
private void oTrans_adapter_AfterAdapterMethod(object sender, AfterAdapterMethodArgs args)
{
switch (args.MethodName)
{
case "Update":
EpiDataView callContextBpmDataView = ((EpiDataView)(this.oTrans.EpiDataViews["CallContextBpmData"]));
string previousQuoteHedSalesRepCode = callContextBpmDataView.dataView[0]["Character01"].ToString();
// Exit if the flag is already cleared (or never existed)
if (previousQuoteHedSalesRepCode == "")
{
return;
}
triggerSalesRepCleanup = true;
EpiDataView edvQuoteHed = ((EpiDataView)(this.oTrans.EpiDataViews["QuoteHed"]));
string currentQuoteHedSalesRepCode = edvQuoteHed.dataView[edvQuoteHed.Row]["SalesRepCode"].ToString();
// Customer change scenario - Epicor replaced the previous Primary Salesperson, so we must reset it. We clear Character01 here since triggerSalesRepCleanup is already true.
if (currentQuoteHedSalesRepCode != previousQuoteHedSalesRepCode)
{
edvQuoteHed.dataView[edvQuoteHed.Row]["SalesRepCode"] = previousQuoteHedSalesRepCode;
callContextBpmDataView.dataView[0]["Character01"] = "";
this.oTrans.Update();
}
break;
}
}
private void edvQSalesRP_EpiViewNotification(EpiDataView view, EpiNotifyArgs args)
{
if ((args.NotifyType == EpiTransaction.NotifyType.Initialize))
{
if ((args.Row > -1))
{
if (!triggerSalesRepCleanup) return;
SalesRepCleanup();
}
}
}
// Delete non-primary reps and set RepSplit to 100 for the primary rep
private void SalesRepCleanup ()
{
foreach(DataRowView rowView in edvQSalesRP.dataView)
{
if (Convert.ToBoolean(rowView["PrimeRep"]) == false)
{
rowView.Delete();
}
if (Convert.ToBoolean(rowView["PrimeRep"]) == true)
{
rowView.Row["RepSplit"] = 100;
}
}
EpiDataView callContextBpmDataView = ((EpiDataView)(this.oTrans.EpiDataViews["CallContextBpmData"]));
callContextBpmDataView.dataView[0]["Character01"] = "";
triggerSalesRepCleanup = false;
this.oTrans.Update();
}
// This just makes sure RepSplit is set to 100 if/when the user manually changes the Primary Salesperson
private void QSalesRP_AfterFieldChange(object sender, DataColumnChangeEventArgs args)
{
switch (args.Column.ColumnName)
{
case "SalesRepCode":
EpiDataView edvQSalesRP = ((EpiDataView)(this.oTrans.EpiDataViews["QSalesRP"]));
if (edvQSalesRP != null && edvQSalesRP.Row > -1)
{
edvQSalesRP.dataView[edvQSalesRP.Row]["RepSplit"] = 100;
}
break;
}
}
}
I’d never used the AfterAdpaterMethod event handler in a customization before now, but working through this problem made it clear how valuable of a strategy it can be. Calling oTrans.Update() was also new to me, also earned my appreciation.
However, using the two together (calling an update inside an after-update event handler) can be like playing with fire as you have to think extremely hard about the code control flow. I always try to avoid situations like that, but alas, this turned out to be my best solution… It came down to that or BPM database updates, which I hate doing.
Andrew, thank you again for your help. Let me also thank @jgiese.wci, @josecgomez, and @timshuwy for your many code snippets throughout this site. They helped a great deal with this.