Default Salesperson on Quote/Orders

We have a minimalist implementation of CRM in Epicor. Territories, Tasks, Campaigns, Reasons, etc. just don’t do much for us, except cause problems.

I’m trying to figure out something that should be very simple (and is simple in other systems) – when a Quote / Order is created (assuming order did not come from a quote), set the Primary / SalesRepCode1 salesperson to the rep linked to the user creating the Quote/Order. And then DON’T CHANGE IT unless a user manually changes it.

We have BPMs that kind of do this (they have some issues) but they are constantly fighting against default Epicor logic and it causes lots of problems. We don’t put Salespeople on Customers or ShipTos, but as far as I know we can’t get around putting one on Territories. So Epicor is always trying to insert the default Territory rep on quotes/orders when they are created or the Customer / ShipTo is changed.

I wish I had a way to just break the default sp logic that is coming from Territories. Then I could deal with the BPM issues no problem. Any ideas?

1 Like

Run a trace to see which BO method is updating the salesperson, and make a BPM (pre-process) to throw an exception, preventing it from completing.

I struggled with this very same problem for a long time on Quotes. I eventually got help at the Solutions Pavilion at Insights one year (from Rich Riley, no less!). The solution was more complicated than I understood at the time. It involves a preprocessing BPM on Quote Update to store the current User ID into a BPMData field. Then a customization on the Quote screen to change the SalesRepCode from whatever base code thinks is right to what we want it be.

I could share more details if desired. What version are you on (I am 10.2.200)?

Andrew, could you share that solution with us.

1 Like

Step 1: Pre-Processing Method Directive on Quote Update


Step 2: Customize the Quote Entry form to change the Sales Rep to remove extra SalesRep and to set the one primary Sales Rep to the value in contextBPMData.character01

// **************************************************
// Custom code for QuoteForm
// Created: 5/9/2014 1:59:17 PM
// **************************************************

using System;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Windows.Forms;
using Ice.Lib;
using Erp.BO;
using Ice.BO;
using Erp.UI;
using Ice.Adapters;
using Erp.Adapters;
using Ice.Lib.Customization;
using Ice.Lib.ExtendedProps;
using Ice.UI.FormFunctions;
using Ice.Lib.Framework;
using Ice.Lib.Searches;

public class Script
{
	// ** Wizard Insert Location - Do Not Remove 'Begin/End Wizard Added Module Level Variables' Comments! **
	// Begin Wizard Added Module Level Variables **

	private EpiBaseAdapter oTrans_adapter;
	private EpiDataView edvQSalesRP;

	// End Wizard Added Module Level Variables **

	// Add Custom Module Level Variables Here **
	// Added for new toolbar Print Preview button

	public void InitializeCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Variable Initialization' lines **
		// Begin Wizard Added Variable Initialization

		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);

		// End Wizard Added Variable Initialization

		// Begin Wizard Added Custom Method Calls

		SetExtendedProperties();
		// End Wizard Added Custom Method Calls

	}

	public void DestroyCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Object Disposal' lines **
		// Begin Wizard Added Object Disposal

		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;

		// End Wizard Added Object Disposal

		// Begin Custom Code Disposal

		// Added for toolbar Print Preview
		// End Custom Code Disposal
	}

	bool didJustUpdate;
	private void oTrans_adapter_AfterAdapterMethod(object sender, AfterAdapterMethodArgs args)
	{
	   if ( args.MethodName == "Update") didJustUpdate = true;
	}
	private void edvQSalesRP_EpiViewNotification(EpiDataView view, EpiNotifyArgs args)
	{
		if (!didJustUpdate) return;
		didJustUpdate = false;
		//MessageBox.Show("In EpiViewNotification.  Starting custom code to delete extra Sales Reps.");
		// If  more than one SalesRep is assigned to the Quote, delete the non-primary ones
		EpiDataView qSalesRPView = ((EpiDataView)(this.oTrans.EpiDataViews["QSalesRP"]));
		foreach(DataRowView rowView in qSalesRPView.dataView)
		{
		if (Convert.ToBoolean(rowView["PrimeRep"]) == false)
		{
			//MessageBox.Show("In foreach and PrimeRep was false" );
			rowView.Delete();
		}
		}
		this.oTrans.Update();
		// now  should be only one remaining primary SalesRep. Set to current user which is in CallContextBPMData.Character01
		EpiDataView callContextBpmDataView = ((EpiDataView)(this.oTrans.EpiDataViews["CallContextBpmData"]));
		if (callContextBpmDataView != null && 
			qSalesRPView != null &&
			qSalesRPView.dataView.Count >0 &&
			qSalesRPView.Row >=0 &&
			!string.IsNullOrEmpty(callContextBpmDataView.dataView[0]["Character01"].ToString()))
	        //  && qSalesRPView.dataView[qSalesRPView.Row]["SalesRepCode"].ToString() != callContextBpmDataView.dataView[0]["Character01"].ToString())
			// last comparison was added 11-14-17 to try to prevent error if SalesRep is already set correctly.
			{
				qSalesRPView.dataView[qSalesRPView.Row].BeginEdit();
				qSalesRPView.dataView[qSalesRPView.Row]["SalesRepCode"] = callContextBpmDataView.dataView[0]["Character01"].ToString();
				qSalesRPView.dataView[qSalesRPView.Row].EndEdit();
				//MessageBox.Show("In EpiViewNotification.  Just set SalesRepCode to " + callContextBpmDataView.dataView[0]["Character01"].ToString());
			}
		callContextBpmDataView.dataView[0]["Character01"] = "";
		this.oTrans.Update();
	}  

}

Good luck!

@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.

7 Likes

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;
  }
}
3 Likes

Hey Tom, I’m in the process of testing E10.2.500.4 right now, and found this post. This seems to be the exact functionality that we thought we were using in E9. Anyway, can you elaborate a little bit on the Customization side of this solution? Is that oTrans.Update() being called in the afterAdapter method for an update, or is it somewhere else?
After implementing you’re solution, I’m still receiving an error when entering a part on the QuoteDtl, “A valid SalesRep Code is required” that refers to the GetSalesRepInfo and QSalesRP.SalesRepCode:


However, when I turn on tracing and take a look at the datasets, they all seem fine to me. The SaleRepCode listed never changes from the SalesRepCode that is set when the quote is first updated. Any thoughts?

Tom,
Nevermind. I had implemented your original version, which had a lot inside the customization layer first. When I saw the updated info from your upgrade experience, I didn’t clear out some of the now unnecessary methods in the customization. I have fixed that now, and everything looks to be running smoothly. Thanks for sharing both iterations of this!
-Jay

Great! I was thinking that might have been the issue. In my “new” solution, the only thing I have left in the customization is this:

I remember having weird issues with the bpmdata epidataview being null, which is why that code looks so goofy… Not very elegant but it seems to work well.

Tom,
One more issue that I ran into while testing some other BPMs. If I update the customer on a quote, I receive the “A valid SalesRep Code is required.” error. At this point, if I manually refresh the entry UI, the customer info reverts back to the original (as expected) but the SalesRep has been deleted and any subsequent change/save process from this point forward allows the SalesRep to remain missing.
-Jay

You said you’re on 10.2.500, right? It very well could be the difference between versions (10.2.700) that has mine working and yours not. I vaguely recall changing the customer to be part of the set of issues I ran into when moving from 10.2.500 to 10.2.700.

Gotcha. I’ll start digging into the logs again and see if I can work it out. If nothing else, I can always go back to your earlier version too. I’ll update here once I work it out.