Create Automatic Follow-Up Quote Tasks

I’ve gotten a request to have Epicor automatically create follow up quote tasks for a sales-closer position when certain criteria is met. What’s the right way to approach it?

I’d rather not do it manually via DMT, since this would be a daily task.

Not sure how I’d do it with BPM’s, since the trigger would be 15 days old…

I could create a BAQ to return results that meet the criteria. How do I schedule something to create new tasks based on that?

Conditions:

  • Quote Entered Date is exactly 90 days old
  • Quote is < $10,000
  • Quote Details contain products from 4 specific product groups
  • No call log notes in the previous 15 days (i.e. original sales rep has let it get stale)
  • LongTerm_c UD field has not been checked
1 Like

Does the “Task” need to be an actual Task in the quote or can it be a reminder? Or some combination of a task and a reminder?
I did something similar, perhaps slightly less complex on the requirements, but utilizing User Process Wrapper to schedule and execute the custom task on a schedule. You’ll probably be able to do it via a Function if you’re on a high enough version.


The conditions you described can be encapsulated in your BAQ, plus any other ones necessary. If that is met, you can proceed with either generating an email or creating a task, etc.

/*Send Follow Up Reminder if QuoteHed.FollowUpDate = Today*/
/*Dictionary queue holds to-be-send emails*/
Dictionary<int, object[]> queue = new Dictionary<int, object[]>();

foreach(var quote in (from q in Db.QuoteHed.With(LockHint.NoLock)
                              
                    /*Join QSalesRp, get SalesRepCode*/
                    join r in Db.QSalesRP.With(LockHint.NoLock)
                      on new { q.Company, q.QuoteNum } equals new { r.Company, r.QuoteNum }
                      
                    /*Join SalesRp to QSalesRP, get EmailAddress*/
                    join s in Db.SalesRep.With(LockHint.NoLock)
                      on new {r.Company, r.SalesRepCode} equals new {s.Company, s.SalesRepCode}
                      
                    /*Join Customer to QuoteHed, get Name*/
                    join c in Db.Customer.With(LockHint.NoLock)
                      on new {q.Company, q.CustNum} equals new {c.Company, c.CustNum}
                      
                    where
                    q.Company == Session.CompanyID 
                    && q.FollowUpDate == DateTime.Today
                    && r.PrimeRep == true
                   
                    select new {
                      q.QuoteNum, q.FollowUpDate, s.EMailAddress, c.Name
                      }
                    ))
                    /*Start Iteration Action*/
                    {
                      if(quote != null)
                      {
                        queue.Add(quote.QuoteNum, new object[] {quote.EMailAddress, quote.FollowUpDate, quote.Name});
                      }
                    }
                    /*End Interation Action*/
/*If queue has any entries, we will create a Epicor Mailer class and send out emails to that person*/        
/*Value is an object array. Access contents by index*/
if(queue.Any())
{
  foreach(var row in queue)
  {
    //debug testing
    /*string path = @"\\<server>\C$\Temp\DictionaryContents.txt"; 
    string content = string.Format("Key: {0}, Value: {1}", row.Key, row.Value);
     if(!File.Exists(path))
     {
     // Create a file to write to.
      string createText = "First Line:" + Environment.NewLine;
    File.WriteAllText(path, createText);
     }
     string appendText = Environment.NewLine + "Response: "+DateTime.Now.ToString() + Environment.NewLine + content;
     File.AppendAllText(path, appendText); 
     */
     //Mailer Helpers
     var mailer = this.GetMailer(async: true); 
     var message = new Ice.Mail.SmtpMail();
     message.SetFrom("fromEmail@yourdomain.com");
     message.SetTo(row.Value[0].ToString());
     //message.SetCC()
     //message.SetBcc() 
     
     DateTime rawDate = (DateTime)row.Value[1];
     var trimDate = rawDate.ToShortDateString();
     string rawBody = string.Format("Quote Follow Up Date set for {0} has been reached for Quote {1}  ({2}).", trimDate,row.Key.ToString(), row.Value[2].ToString());
     string htmlWrapperStart = "<html><body><p>";
     string htmlWrapperEnd = "</p></body></html>";
     string finalBody = string.Format("{0}{1}{2}", htmlWrapperStart, rawBody, htmlWrapperEnd);
     
     message.SetBody(finalBody);
     message.Subject = string.Format("Follow Up Reminder: Quote {0}", row.Key.ToString());
     //Dictionary<string, string> attachments = new Dictionary<string, string>();
     //attachments.Add("MyFirstAttachment", @"\\MyServer\myFolder\MyFile.pdf");
     mailer.Send(message);
  }
  queue.Clear();
}   
else
{
  /*
  //debug testing
  string path = @"\\<server>\C$\Temp\BPMDebugger.txt";
  string content = string.Format("No Dictionary Values. DateVar = {0}", DateTime.Today.ToString());
   if(!File.Exists(path))
   {
   // Create a file to write to.
    string createText = "First Line:" + Environment.NewLine;
  File.WriteAllText(path, createText);
   }
   string appendText = Environment.NewLine + "Response: "+DateTime.Now.ToString() + Environment.NewLine + content;
   File.AppendAllText(path, appendText); 
 */
} 

You could probably do it all with widgets if you want.

Then, it’s a matter of scheduling the process or function in Epicor to run daily (or whatever the requirement is).

There is a standard follow-up alert functionality built into data directive AlertQue:


I have custom code composing slightly different emails when quotes are first quoted (1010), when the quote expires(1020), and when the quote reaches its follow up date as defined in the quote header tab (1030). But, you could do anything you want once you see the AlertNum = 1030 in the directive.

foreach (var ttAlertQue_Row in ttAlertQue)
{
    int AlertNum = ttAlertQue_Row.Alertnum;  // used to determine what kind of alert this is.
    
    // find the QuoteNum from the AlertQue text.
    int ttQuoteNum = Convert.ToInt16(ttAlertQue_Row.AlertText.Substring(7,5));

    // find the Quote_Hed row for the new quote
    var QuoteHed_Row = (from QuoteHed_temp in Db.QuoteHed
    where QuoteHed_temp.Company == Session.CompanyID
    && QuoteHed_temp.QuoteNum == ttQuoteNum
    select QuoteHed_temp).FirstOrDefault();

    // find the Customer record for that Quote_Hed
    var Customer_Row = (from Customer_temp in Db.Customer
    where Customer_temp.Company == Session.CompanyID
    && Customer_temp.CustNum == QuoteHed_Row.CustNum
    select Customer_temp).FirstOrDefault();

    // now have quote number and customer name so can start email body and subject line
    switch(AlertNum)
        {
        case 1010:  // Quoted Quote
            Email_Subject =  "Quote " 
            + System.Convert.ToString(QuoteHed_Row.QuoteNum) 
            + " was sent to Customer: " + Customer_Row.Name 
            + " (" + Convert.ToString(Customer_Row.CustID) + ") for $ "
            + Convert.ToString(Math.Round(QuoteHed_Row.TotalGrossValue,2));
            break;

        case 1020:  // Expired Quote
            Email_Subject =  "Quote " 
            + System.Convert.ToString(QuoteHed_Row.QuoteNum) 
            + " has Expired for Customer: " + Customer_Row.Name 
            + " (" + Convert.ToString(Customer_Row.CustID) + ") for $ "
            + Convert.ToString(Math.Round(QuoteHed_Row.TotalGrossValue,2));
            break;
            
        case 1030:  // Follow-up Quote
            Email_Subject =  "Quote " 
            + System.Convert.ToString(QuoteHed_Row.QuoteNum) 
            + " has reached Follow-up date for Customer: " + Customer_Row.Name 
            + " (" + Convert.ToString(Customer_Row.CustID) + ") for $ "
            + Convert.ToString(Math.Round(QuoteHed_Row.TotalGrossValue,2));
            break;
        }

There something slightly quirky about when the follow-up alert trigger runs on any given day. It doesn’t run until one person opens the Quote Entry screen. So, over the weekend for example, the follow-up messages don’t come out until Monday morning.

4 Likes

That’s exactly why I rolled my own :wink:

Don’t know if this helps, I run this on a Sys Agent Schedule that fires off each morning, you could add more conditions into it and add a foreach on the Db.QuoteDtl table to check your product groups

Its on a Standard Data Directive

image

I have another BPM that sets the followup date, but you can also set that in the company configuration

bool tvSYS = ttSysAgentSched.Any(s => s.Updated() && ttSysAgentSched.Any(s1 => s1.Unchanged() && s1.SysRowID == s.SysRowID && s1.LastRunOn != s.LastRunOn && s.AgentSchedNum == 177181));
if (tvSYS == true)
{
var quotes = Db.QuoteHed.Where(q => q.Company == Session.CompanyID && q.FollowUpDate == DateTime.Today && q.QuoteClosed == false /* add more conditions here */ ).Select(x => new {x.QuoteNum, x.BTCustNum, x.ChangedBy, x.Company}).ToList(); 
if(quotes != null)
{
foreach(var quote in quotes) 
{
var qNum = quote.QuoteNum;
var qCust = Db.Customer.Where(c=> c.Company == Session.CompanyID && c.CustNum == quote.BTCustNum).Select(x => x.Name).FirstOrDefault();
var qepEm = quote.ChangedBy + "@company.com.au; ";
var qCompany =quote.Company;
//Rep Details
var saRep = Db.QSalesRP.Where(q => q.Company == qCompany && q.QuoteNum == qNum && q.PrimeRep == true).Select(x => x.SalesRepCode).FirstOrDefault();
var soRepE = Db.SalesRep.Where(rp => rp.SalesRepCode == saRep).Select(x => x.EMailAddress).FirstOrDefault();
var emailTo = qepEm + " " + soRepE;
var emailSub = "Quote Number " + qNum + " is due for follow up today";
string emailBody = "<html><body><p><b>Hello<br><br>Quote Number " + qNum + " for " + qCust + " is due for follow up today<br><br>Following up your quotes increases the quote conversion rate</b><br><br>*** This is a system generated email ***  </p></body></html>";

var mailer = this.GetMailer(async: true);
var  message = new Ice.Mail.SmtpMail();
message.SetFrom("do-not-reply@company.com.au");
message.SetTo(emailTo);
//   message.SetReplyTo();
//   message.SetCC();
//   message.SetBcc(); 
message.SetSubject(emailSub);
message.SetBody(emailBody);
message.IsBodyHtml = true;
mailer.Send(message);
}
}
}

Thank you guys! This gives me a lot to start working with.

@Aaron_Moreng - It does need to be a task. We’ve pushed using Epicor’s quote tasks in 2020, instead of folks working outside the system. It’s been helpful to the managers to see activity (or lack). We’re starting to add tasks to non-sales departments that are involved in the project quoting process too. I’ll check out the UserProcessWrapper. Glad I can schedule that with a BAQ inside. We’re on 10.2.500, upgrading to .700 in 2 weeks (that shiny new Epicor version. Oooooh!). I’ve been meaning to start using functions. This would be a good start. The code you attached is a big help too.

@alintz - Thanks. Last time I looked into the AlertQue and Global Alerts, it was pretty limited. I hadn’t thought about working with the follow-up date, but that’s an idea.

@LBARKER - Thank you. Good to know of that option too. I hadn’t seen that before!

You guys rule. Have a great weekend.