Trying to Use Epicor Function to send email with Multiple records in it

Hello,

I am trying to set up a recurring function that checks if a BAQ has any records in it and then emails that data to someone.

I have the BAQ built and marked as updateable so I can create a Base BPM Directive to send the email.

What I am not sure about is how to get the data into the email.

Is there a way to dump the BAQ data resiults into a Dataset and have the email show that?

Is there a better way to get all the BAQ results into the email?

Any help is appreciated.

We are still using the Modern style in the client if that makes a difference.

Back up just a bit, and tell us the whole story.

What, Why, How you would like it presented, etc.

Renovate Channel 9 GIF by The Block

1 Like

I have been asked to send an email twice a day at certain times letting a person know that there are still pending Purchase Orders.

I am just now starting to learn functions and came across a youtube video which walked me through creating an epicor function that will use a uBAQ to schedule triggering it.

I have tested it for a different project I had and it worked fine. That project only returns 1 or 0 rows, so it wasn’t an issue.

This project could return many rows of pending POs.

In the video, they had me build a uBAQ that returns the rows I need.

Then go to the update tab->Update Processing tab and select ‘Advanced BPM Update only’.

I then click the ‘BPM Directived Configuration’ button and select the Update method.

Then I add a base processing to it and set that up to do the email.

It is at this point I am stuck.

In my last project I had to create a lot of variables to hold the data from the one row.

I am not sure how it would work with multiple rows.

I thought maybe I could create an empty dataset variable and populate it with the BAQ results but wasn’t sure how to do it, if possible.

If there is a better way, I will take it.

When you open the email template and right click, there are not many choices in there to use (screenshot below).

image

If you have any other questions, please let me know.

My opinion would be to create a BAQ Report, add that to the menu, create a report style that emails the report when selected, and make process set to schedule the task.

2 Likes

I’ve not done this exact thing but if you left click on the Insert button do you get Table Query, IIRC that will pull in the value you select as a table

Maybe an easier thing would be C# code to combine the PONums into a comma delimited string.

How would I reference the BAQ results in the C#?

I agree with @dakhit . I would create the BAQ to the identify the ‘Arrived’, create the associated BAQ Report, and use the Advanced Print Routing functionality, to create and email as an attachment, and then assign to an appropriate schedule. If ‘Arrived’ items exist the email is sent and if not, there is no email.

1 Like

BAQ Report is definitely the most straightforward way to set that up.

However, I had a few minutes when waiting for some food in the oven so…

Below will be some steps to set up an Epicor Function that accepts a ~ delimited list of emails and the name of a BAQ and will convert it into a CSV and email it to that list of recipients. There is some minor conversion of headers in there since I don’t like the TableName_FieldName convention so it will strip the table name out and add spaces to the field name on capital letters. Feel free to change any of the logic to do what you want instead.

DISCLAIMER: No warranty or support or any other additional future help will be provided along with the code that I provide and I will not be responsible for anyone running any code that I provide. So, please use at your own risk and review the code thoroughly before running it to ensure you understand what it is doing :slight_smile:

Instead of just uploading the efxj export I will just list the steps to follow

  1. Create a new function library (or use an existing one)

  2. Add the following references:

  3. Create a new C# function (name it whatever you want) and add the following parameters (please note that I did not create parameters for FromEmail, Subject, or Body but feel free to add them to make the function more dynamic):

  4. Paste the following code into your function:

try
  {
      #region Variables

      var recipients = Recipients.Split('~').ToList();
      var csvData = "";

      #endregion

      #region Helper Functions

      Func<DataSet, string> convertDataSetToCsvString = (ds) =>
      {
          if (ds == null)
          {
              return "";
          }

          DataTable dataTable = null;

          if (ds.Tables.Contains("Results"))
          {
              dataTable = ds.Tables["Results"];
          }
          else
          {
              // If "Results" table is not found, it means the DataSet structure doesn't match
              // the expected format. 
              return "";
          }

          // If the table has no columns, we can't generate headers or meaningful CSV.
          if (dataTable.Columns.Count == 0)
          {
              return "";
          }

          var csvBuilder = new System.Text.StringBuilder();

          Func<string, string> formatHeader = (columnName) =>
          {
              string namePartToFormat;
              string[] parts = columnName.Split('_');

              // "splitting on the _ and then taking the second part"
              if (parts.Length >= 2)
              {
                  namePartToFormat = parts[1];
              }
              else
              {
                  // No underscore, or only one part after split; use the original name (or part)
                  namePartToFormat = parts[0];
              }

              if (string.IsNullOrEmpty(namePartToFormat))
              {
                  return ""; // Return an empty string for an empty header part
              }

              // "adding a space between capital letters"
              // Example: "OrderNum" -> "Order Num"
              System.Text.StringBuilder headerBuilder = new System.Text.StringBuilder();
              headerBuilder.Append(namePartToFormat[0]);
              for (int i = 1; i < namePartToFormat.Length; i++)
              {
                  if (char.IsUpper(namePartToFormat[i]))
                  {
                      headerBuilder.Append(' ');
                  }

                  headerBuilder.Append(namePartToFormat[i]);
              }

              return headerBuilder.ToString();
          };

          // Helper function to escape fields for CSV format
          Func<object, string> escapeCsvField = (fieldValue) =>
          {
              if (fieldValue == null || fieldValue == DBNull.Value)
              {
                  return "";
              }

              string field = fieldValue.ToString();
              // If field contains a comma, double quote, or newline characters,
              // enclose it in double quotes and escape any existing double quotes by doubling them.
              if (field.IndexOfAny(new char[] { ',', '"', '\r', '\n' }) != -1)
              {
                  return "\"" + field.Replace("\"", "\"\"") + "\"";
              }

              return field;
          };

          // Generate Headers
          var headers = new List<string>();
          foreach (DataColumn column in dataTable.Columns)
          {
              // Headers themselves should be escaped in case the formatted header contains special characters
              headers.Add(escapeCsvField(formatHeader(column.ColumnName)));
          }

          csvBuilder.AppendLine(string.Join(",", headers));

          // Generate Data Rows
          // Only proceed if there are rows to process
          if (dataTable.Rows.Count > 0)
          {
              foreach (DataRow row in dataTable.Rows)
              {
                  var fields = new List<string>();
                  foreach (DataColumn column in dataTable.Columns)
                  {
                      fields.Add(escapeCsvField(row[column]));
                  }

                  csvBuilder.AppendLine(string.Join(",", fields));
              }
          }
          // If dataTable.Rows.Count is 0, the csvBuilder will just contain the header line.

          return csvBuilder.ToString();
      };

      Func<string, byte[]> convertCsvStringToByteArray = (csv) =>
      {
          if (string.IsNullOrEmpty(csv))
          {
              return new byte[0]; // Return an empty byte array for null or empty string
          }

          return System.Text.Encoding.UTF8.GetBytes(csv);
      };


      Action<string, List<string>, string, string, Dictionary<string, byte[]>> sendEmail =
          (fromEmail, recipientList, subject, body, attachments) =>
          {
              using (var smtpClient = new Ice.Mail.SmtpMailer(Session))
              {
                  // Create the actual email 
                  var mailMessage = new Ice.Mail.SmtpMail
                  {
                      From = fromEmail,
                      To = recipientList,
                      Subject = subject,
                      Body = body
                  };

                  // Send the email with your attachment(s)
                  smtpClient.Send(mailMessage, attachments);
              }
          };

      #endregion


      #region Get BAQ Results

      CallService<Ice.Contracts.DynamicQuerySvcContract>(svc =>
      {
          // Can be empty - Or set values to filter the results
          var executionParams = new QueryExecutionTableset();
          var baqResults = svc.ExecuteByID(BAQName, executionParams);

          // Now we can parse the results using our helper method 
          var csv = convertDataSetToCsvString(baqResults);

          // Save the csv data for later use
          csvData = csv;
      });

      #endregion


      #region Send Email

      // Make sure our csv data is in the correct format
      var csvDataAsByteArray = convertCsvStringToByteArray(csvData);

      // Create our attachments dictionary
      var mailAttachments = new Dictionary<string, byte[]>();
      mailAttachments.Add("BAQResults.csv", csvDataAsByteArray);

      // Send the email :)
      sendEmail("epicor@epicor.com", recipients, "BAQ Results", "Attached are the results of the BAQ", mailAttachments);

      #endregion
  }
  catch (Exception e)
  {
      IsSuccess = false;
      Message = e.Message;
  }

Please note that I wrote this hastily but tested and it does work. It is currently set up to just support a standard BAQ that contains no input parameters so keep that in mind.

  1. Test the function out by using the “Schedule Epicor Function” menu item:
    image

  1. If all works well, just set up a schedule in the System Agent menu and then run that with the recurring checkbox checked.
    image

Good luck! :slight_smile:

2 Likes

Oh you can’t do that here.

You are forever obligated even after your death to answer questions like to a two year old.

Read Rebecca Wisocky GIF by CBS

1 Like

Scared Homer Simpson GIF by reactionseditor

1 Like

Oh shit, It’s Eli lol.

Hey dude.

One of my very rare posting days haha

Hello! Hope all is going well :slight_smile:

1 Like

This worked for me.

I did modify it to have the PO numbers show in the body of the email.

That way they don’t have to open the CSV unless they want all the fields.

I really appreciate you putting this together for me. It is awesome!

1 Like

This is best achieved via a Function that you can schedule to run at certain points in the day, used in conjunction with User Codes you can get a really nice email to let people know the status:

Happy to help with how to achieve this.

2 Likes

I am always open to learning a new/different/better way to do things.

What do the user codes do for this?

How did you get it to do a table for the information?

That link is pretty awesome looking also, will it work for Gov Cloud?

If you have the time to walk me through this, I, and other future people having this question would appreciate you taking the time, if possible.

Hi
User Codes allow the configuration of various “standard” formatting elements that can be used within the Function, do for example the User Code for the PO Approval Function is setup as follows:

Then I have a BAQ that returns a list of PO’s awaiting approval:

Then within the Function, I invoke the Ice.MailSetting.GetByID BO to get the “from email address” that is part of the company setup and taking that value into a CallContextBPMDataField, next is a Custom Code widget where i call the BAQ, take the results and format the email from the User Codes - here is the custom code:

//string baqID = "EPA_EmailPOSubForApproval";
string baqID = string.Empty;
string approverNamePlaceholder = "#Name";
Ice.Tables.UDCodes UDCodes = null;

foreach(var URL in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "URL"
  select ThisUDCodes))
  callContextBpmData.Character02 = URL.LongDesc;
  
foreach(var FontFamily in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "Font"
  select ThisUDCodes))
  callContextBpmData.Character03 = FontFamily.LongDesc;
  
foreach(var FontColour in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "FontColour"
  select ThisUDCodes))
  callContextBpmData.Character04 = FontColour.LongDesc;
  
foreach(var HRSBACKCOL in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "HRSBACKCOL"
  select ThisUDCodes))
  callContextBpmData.Character05 = HRSBACKCOL.LongDesc;

foreach(var HRSCOLOUR in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "HRSCOLOUR"
  select ThisUDCodes))
  callContextBpmData.Character06 = HRSCOLOUR.LongDesc;
  
foreach(var TRSCOLOUR in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "TRSCOLOUR"
  select ThisUDCodes))
  callContextBpmData.Character07 = TRSCOLOUR.LongDesc; 
  
  foreach(var TRSCOLOUR in (from ThisUDCodes in Db.UDCodes.With(LockHint.UpdLock) where
  ThisUDCodes.Company == Session.CompanyID &&
  ThisUDCodes.CodeTypeID == "POApproval" &&
  ThisUDCodes.CodeID == "BAQID"
  select ThisUDCodes))
  baqID = TRSCOLOUR.LongDesc; 


// HTML
string divStart = "<div style='font-family:" + callContextBpmData.Character03 + "; color:" + callContextBpmData.Character04 + ";'>";
string htmlTableStart = "<table style=\"border-collapse:collapse; text-align:center; font-family:" + callContextBpmData.Character03 + "; \" >";
string htmlTableEnd = "</table>";
string htmlHeaderRowStart = "<tr style =\"background-color:" + callContextBpmData.Character05+ "; color:" + callContextBpmData.Character06 + ";\">";
string htmlHeaderRowEnd = "</tr>";
string htmlTrStart = "<tr style='color:" + callContextBpmData.Character07 + ";'>";
string htmlTrEnd = "</tr>";
string htmlTdStart = "<td style=\" border-color:#a5a5a5; border-style:solid; border-width:thin; padding: 5px;\">";
string htmlTdEnd = "</td>";
string divEnd = "<br>This email was automatically generated from Epicor</div>";

//Email
string emailFrom = callContextBpmData.Character01;
string emailSubject = "PO Approval Notification";
string emailBodyTemplate = divStart + "Dear " + approverNamePlaceholder + ",<br><br>";

emailBodyTemplate += "The below Purchase Order(s) are awaiting Approval/Rejection: <br><br>";
emailBodyTemplate += "Follow this link to open PO Approvals: <br><br>";
string link = callContextBpmData.Character02;
emailBodyTemplate += "<a href='" + link + "'>" + link + "</a>"+"<br><br>";

emailBodyTemplate += htmlTableStart;
emailBodyTemplate += htmlHeaderRowStart;
emailBodyTemplate += htmlTdStart + "PO Number" + htmlTdEnd;
emailBodyTemplate += htmlTdStart + "Supplier ID" + htmlTdEnd;
emailBodyTemplate += htmlTdStart + "Supplier Name" + htmlTdEnd;
emailBodyTemplate += htmlTdStart + "Buyer" + htmlTdEnd;
emailBodyTemplate += htmlHeaderRowEnd;

DataTable dtResults = null;

this.CallService<Ice.Contracts.DynamicQuerySvcContract>(
  query =>
  {
      Ice.Tablesets.QueryExecutionTableset dsQueryExecution = new QueryExecutionTableset();
      DataSet dsResults = query.ExecuteByID(baqID, dsQueryExecution);
      dtResults = dsResults.Tables[0];
  }
);

var mailer = this.GetMailer(async:false);
Ice.Mail.SmtpMail message = new Ice.Mail.SmtpMail();

string currentApprover = string.Empty;
string emailBody = string.Empty;

for (int i = 0; i < dtResults.Rows.Count; i++)
{
  DataRow currentRow = dtResults.Rows[i];
  
  if (i == 0)
  {
    currentApprover = currentRow["Calculated_Name"].ToString();
    
    message.SetFrom(emailFrom);
    message.SetTo(currentRow["Calculated_ApproverEmail"].ToString());
    
    if (!string.IsNullOrEmpty(CCEmail))
    {
      message.SetCC(CCEmail);
    }
    
    message.SetSubject(emailSubject);
    emailBody = emailBodyTemplate;    
  }

  if (!String.Equals(currentApprover, currentRow["Calculated_Name"].ToString()))
  {
    // End table
    emailBody += htmlTableEnd + divEnd;
    emailBody = emailBody.Replace(approverNamePlaceholder, currentApprover);
    
    // Send email
    message.SetBody(emailBody);
    message.IsBodyHtml = true;
    mailer.Send(message);
    
    // Log details to file
    
    
    // Set currentApprover
    currentApprover = currentRow["Calculated_Name"].ToString();
    
    // Reset message
    message = new Ice.Mail.SmtpMail();
    message.SetFrom(emailFrom);
    message.SetTo(currentRow["Calculated_ApproverEmail"].ToString());
    
    if (!string.IsNullOrEmpty(CCEmail))
    {
      message.SetCC(CCEmail);
    }
    
    message.SetSubject(emailSubject);  
    emailBody = emailBodyTemplate;

    emailBody += htmlTrStart;
    emailBody += htmlTdStart + currentRow["POApvMsg_PONum"].ToString() + htmlTdEnd;
    emailBody += htmlTdStart + currentRow["Vendor_VendorID"].ToString() + htmlTdEnd;
    emailBody += htmlTdStart + currentRow["Vendor_Name"].ToString() + htmlTdEnd;
  emailBody += htmlTdStart + currentRow["PurAgent_Name"].ToString() + htmlTdEnd;
    emailBody += htmlTrEnd;    
  }
  
  else
  {
    emailBody += htmlTrStart;
    emailBody += htmlTdStart + currentRow["POApvMsg_PONum"].ToString() + htmlTdEnd;
    emailBody += htmlTdStart + currentRow["Vendor_VendorID"].ToString() + htmlTdEnd;
    emailBody += htmlTdStart + currentRow["Vendor_Name"].ToString() + htmlTdEnd;
  emailBody += htmlTdStart + currentRow["PurAgent_Name"].ToString() + htmlTdEnd;
    emailBody += htmlTrEnd;
  }
  
  if (i == dtResults.Rows.Count - 1)
  {
    // End table
    emailBody += htmlTableEnd + divEnd;
    emailBody = emailBody.Replace(approverNamePlaceholder, currentApprover);
    
    // Send email
    message.SetBody(emailBody);
    message.IsBodyHtml = true;
    mailer.Send(message);
  }
}

I reuse this in loads of areas, not just PO Approvals…remove the hyperlink, change the BAQ id and you can get email notifications around PO arrivals (complete/short), requisition arrivals and all sorts of other things.

Taking it one step further, you can create a single User Code containing everything that you need, a single Function Library containing and functions within that for notification areas, again recyling the same code and just changing some references

3 Likes

Does the function need any references added to it?

I am about to start inputing your code and was wondering before I got too far into it.

I just put the code in an ‘Execute Custom Code’ block and it returned an error:

BPM009 Member declaration is not allowed inside code block

Not sure what that means.

Hello,

I have made it a bit into this and have some questions.

Then within the Function, I invoke the Ice.MailSetting.GetByID BO to get the “from email address” that is part of the company setup and taking that value into a CallContextBPMDataField, next is a Custom Code widget where i call the BAQ, take the results and format the email from the User Codes - here is the custom code

First, I did a reference->Service to: “ICE.BO.MailSetting”, would that be correct?

I then tried to do ‘Invoke BO Method’ inside the design section.

I set the Company parameter to the company I am using “OVR” and set the to var: ds

I then did a ‘set field’ widget and used it to set: ds.MailSetting.EmailFromAddr field of the changed row to the CallContectBpmData.Character01 expression

Then I attached the ‘Execute Custom Code’ widget and pasted your code directly into it.

When I run the ‘Check Syntax’, I get the same error as before: BPM009 Member declaration is not allowed inside code block

Not sure what that means.

Any ideas?

Also, does what I set up sound correct?

Hello,

Forget that last error. I puzzled out that the issue was from where I copied and pasted from.

I originally copied your code to word to make it easier to read.

When I did that, it changed the double quotes to a different version that Epicor hates.

Once I corrected that, the “Member declaration is not allowed inside code block” error went away.

I had to add a table reference to Ice.UDCodes to get rid of a Db error.

I am getting closer, I hope.

I did have to add an assemby reference to: Ice.BO.DynamicQuery to clear up a couple of other errors.

The current error I am working on are:

The name ‘CCEmail’ does not exist in the current context

If you have any ideas, let me know.

Otherwise I will keep poking at it to see if I can clear them up.