Skip to content

Instantly share code, notes, and snippets.

@lockworld
Last active January 3, 2024 11:38
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lockworld/ea78dc42fd9a5f327f2ed2dfedc828eb to your computer and use it in GitHub Desktop.
Save lockworld/ea78dc42fd9a5f327f2ed2dfedc828eb to your computer and use it in GitHub Desktop.
Code snippets intended to be reused throughout the Epicor 10 implementation.

A set of gists for re-use throughout various Epicor customizations.

/*
Add this function to the beginning of any BPM directives and utilize whenever logging is needed.
This function can use predefined UDCodes to parse the logEnabled flag and the log path.
Updating either of these user codes will change the logging behavior for all debug logs written in the current environment that utilize this code. This gives developers a one-stop shop for turning logging on/off and configuring the folder path for the log specific to each environment.
I set a string called timeinticks to the current time measured in ticks (10,000 ticks per millisecond) to act as a unique identifier for each iteration of the BPM. This has helped in the past when the same BPM was being fired by different processes, and each process was writing to the same log file.
I set a local boolean called debugMode in each BPM so a developer can turn on logging for a single BPM for troubleshooting purposes without turning on logging for all BPMs using this snippet. This boolean will not only override the global logEnabled flag, but will also append the timeinticks variable to the log to ensure that each iteration of the BPM is writing to a unique log file instead of sharing the same log file.
This code can be modified to work in a form customization as well as a BPM. Just make it into a private method instead of a delegate.
Required variables:
--logActive: If set to false,the function does nothing. Can be set globlly using UserCodes.
--logPath: UDCode with "LongDescription" set to the network path for writing logs to the server. Must be a path the end user can access.
--timeinticks: A unique identifier specific to this instance of the BPM
--debugMode: A local boolean that can override the global logActive variable (used for troubleshooting a single BPM in production)
*/
//Set the global logging variables
bool logActive = true; // Set logging to true by default
var writeLog = Db.UDCodes.FirstOrDefault(u=>u.CodeTypeID.ToUpper()=="{CodeTypeID}" && u.CodeID.ToLower()=="writelogs"); // Look up the global variable to enable or disable logging
bool.TryParse(writeLog.CodeDesc, out logActive); // Parse the UDCode's description field as a boolean (If unable to parse, the initial value of logActive is used)
var logPath = Db.UDCodes.FirstOrDefault(u=>u.CodeTypeID.ToUpper()=="{CodeTypeID}" && u.CodeID.ToLower()=="logpath");
//Set the local variables
string timeinticks = DateTime.Now.Ticks.ToString(); // Unique identifier for this iteration of the BPM
bool debugMode=false; // Local variable to override logActive and to force each iteration of the BPM to write to a unique file
Func<string, string, bool> WriteLog = delegate(string message, string level)
{
if (logActive || DebugMode)
{
try
{
string lvl;
switch(level.ToLower())
{
case "debug":
lvl="00-DEBUG";
break;
case "info":
lvl="01-INFORMATION";
break;
case "information":
lvl="01-INFORMATION";
break;
case "warning":
lvl="02-WARNING";
break;
case "error":
lvl="03-ERROR";
break;
default:
lvl="04-" + level;
break;
}
string logFile = logPath.LongDesc.Trim('/')
+ "/" + this.Name + "_Log-"
+ DateTime.Now.ToString("yyyy-MM-dd")
+ (DebugMode ? "_Iteration-" + timeinticks : "") // Separate log files for each run of BPM
+ ".csv";
if (!System.IO.File.Exists(logFile))
{
System.IO.File.AppendAllText(logFile,
"\"Timestamp\",\"Method\",\"Iteration\",\"User\",\"Level\",\"Message\""
+Environment.NewLine
);
}
System.IO.File.AppendAllText(logFile,
"\"" + DateTime.Now.ToString() + "\","
+ "\"" + this.Name + "\","
+ "\"I-" + timeinticks + "\","
+ "\"" + Session.UserID + "\","
+ "\"" + lvl + "\","
+ "\"" + message.Replace("\"","''") + "\""
+ Environment.NewLine
);
}
catch (Exception ex)
{
// Fail silently so we can continue on if the log is being accessed by another process or the log path can't be found
}
}
return true;
};
Func<string, string> Debug = delegate(string msg)
{
if (DebugMode)
{
WriteLog(msg, "Debug");
}
return "";
};
WriteLog("Staring BPM...","Info"); // This message will be logged if debug=true or logActive=true
Debug("This message will only show if debug=true"); // This message will be logged if debug=true, regardless of the logActive value
/*
This fuction seialzes any object to XML. Very useful when debugging Epicor business object method call that take Tablesets as parameters.
*/
Func<Object,string> SerializeObj = delegate(Object o)
{
try
{
System.Xml.Serialization.XmlSerializer x = new System.Xml.Serialization.XmlSerializer(o.GetType());
TextWriter WriteFileStream = new StreamWriter(@"\\Server\Share\Logs\" + o.ToString() + ".xml");
x.Serialize(WriteFileStream, o);
WriteFileStream.Close();
WriteLog("Serializing " + o.ToString() + " succeeded.");
}
catch (Exception ex)
{
// I cath and ignore errors on my troubleshooting functions
}
return "";
};
var OrderAllocListTS = new Erp.Tablesets.OrderAllocListTableset();
SerializeObj(OrderAllocListTS);
/*
Had a thought tht we could easily create "featue flags" in Epicor that old enable us to turn on/off whole sets of customizations (Primarily BPMs) by using a secial UD Code Type ID to control whether features were turned on or off.
In any BPM you crate, add a variable named FeatureXEnabled (Wheere "X" is the name of the feature you want to check).
Next, add a custom code block with the following code:
*/
var featurexflag = Db.UDCodes.FirstOrDefault(u=>u.CodeTypeID=="FeatureFlg" && u.CodeID.ToLower()=="featurex");
FeatureXEnabled = (featurexflag!=null ? featurexflag.IsActive : false);
/*
Finally, add a Condition block to check whether FeatureXEnabled is true or false. If true, continue executing your BPM code. If false, stop the process.
This way, BPMs can be moved into production enviroments, but will remain inactive until someone adds th correct FeatureFlg and sets the "Active" column to true.
*/
/*
Add this function to the beginning of any BPM under development and utilize whenever you need to monitor the data being transmitted back and forth.
This function will use reflection to find the name and value of every property associated with the object passed in. Use this to see what data is being passed with each row in a temporary table, etc.
This function is for use on single objects. Use with a foreach clause to iterate through multiple instances of an object (Such as rows in a temporary table, etc.).
*/
using System.ComponentModel;
Func<object, string> GetProperties = delegate(object obj)
{
var msg = "";
try
{
for (var i=0; i<TypeDescriptor.GetProperties(obj).Count; i++)
{
var descriptor = TypeDescriptor.GetProperties(obj)[i];
var curVal = "";
try
{
curVal = (descriptor.GetValue(obj) ?? "").ToString();
}
catch (Exception ex)
{
curVal=ex.Message.ToString();
}
msg += descriptor.Name + "=" + curVal + "\r\n";
}
}
catch (Exception ex)
{
msg += "\r\nAN ERROR HAS OCCURRED:\r\n" + ex.ToString();
}
msg += "\r\n";
return msg;
};
/*
Title: Check to see if we need to show Developer Message
Type: Standard Code Snippet
Requirements: (Values should be set in a previous process)
string DeveloperMessage
bool ShowDeveloperMessage
Author: Doug Lockwood
Date: 12/13/2017
Usage: This snippet is intended for use by any Epicor developer. Add this as a custom executable code block at the end of any BPM to display any DeveloperMessage strings created as an information message at the end of the BPM process. This snippet can be customized by adding UserIds to the (callContextClient.CurrentUserId.ToLower()=="dlockwood") condition. For questions, contact Doug Lockwood.
Modifications:
Date: Summary:
12/14/17 Modified to check for blank DeveloperMessage before anything else (If there is no message, there is no need to decide whether or not to display it).
*/
if (!String.IsNullOrEmpty(DeveloperMessage))
{
// Provide information about the current BPM
DeveloperMessage = "--------------------\r\n"
+ "{Current BPM Information}"
+ "\r\n--------------------\r\n"
+ DeveloperMessage + "\r\n";
// Show developer messages to Doug Lockwood always
if (callContextClient.CurrentUserId.ToLower()=="dlockwood")
{
ShowDeveloperMessage=true;
DeveloperMessage += "\r\n----------\r\n* Showing Developer Messages because current user is " + callContextClient.CurrentUserId + ".\r\n";
}
// Show developer messages to anyone in the DEV environment only
var curCompany = Db.Company.FirstOrDefault(x=>x.Company1==callContextClient.CurrentCompany && x.Name.ToLower().Contains("dev"));
if (curCompany!=null)
{
ShowDeveloperMessage=true;
DeveloperMessage += "\r\n----------\r\n* Showing Developer Messages because company \"" + curCompany.Name + "\" is a development company.\r\n";
}
// Queue up a Developer Message to display at the end of the process
if (ShowDeveloperMessage)
{
DeveloperMessage = "DEVELOPER MESSAGE*:\r\n====================\r\n"
+ DeveloperMessage
+ "\r\n\r\n====================\r\n"
+ "Assembly: " + callContextClient.AssemblyName + "\r\n"
+ "CGC: " + callContextClient.CGCCode + "\r\n"
+ "Client Type: " + callContextClient.ClientType + "\r\n"
+ "Current Company: " + callContextClient.CurrentCompany + "\r\n"
+ "Current Plant: " + callContextClient.CurrentPlant + "\r\n"
+ "Current User ID: " + callContextClient.CurrentUserId + "\r\n"
+ "Customization: " + callContextClient.CustomizationId + "\r\n"
+ "Process: " + callContextClient.ProcessId + "\r\n"
+ "====================\r\n";
this.PublishInfoMessage(DeveloperMessage,
Ice.Common.BusinessObjectMessageType.Information,
Ice.Bpm.InfoMessageDisplayMode.Individual,
"Developer Message",
"Information From the Developer");
}
}
//Use these comments for the main script section
/***********************************************
Customization: {Current Customization Name}
Date: {Date Created}
Author: {Primary Developer}
Purpose: {Brief description of purpose}
***********************************************/
//Use these comments for in-line comments or method comments
/***********************************************
Summary:
{High-level overview of code block}
Inputs:
Outputs:
Created by:
Created on:
Change Log:
Initials Date Description
***********************************************/

By default, all customizations should be marked with "All Companies" (so that if we happen to add new companies in the future, our customizations will be available to them).

When saving a new customization, add the following header block: /*********************************************** Based on: {Previous Customization Name}

Date: {Date Created}

Author: {Primary Developer}

Purpose: {Brief description of purpose}

***********************************************/

Example App.PartEntry.PartForm customization "02_Phase_II_Enhancements":
/***********************************************
Based on: 26774_Part_Master_Customization

Date:       10/19/2017
Author:     Doug Lockwood
Purpose:  Modifications to Part Master required for Phase II Rollout

***********************************************/

Examples:�02_Phase_II_Enhancements Number="02" Customization Name="Phase_II_Enhancements"

//Place these comments in the first Custom Code block of a BPM
/***********************************************
BPM: {Current BPM Path & Name}
Date: {Date Created}
Author: {Primary Developer}
Purpose: {Brief description of purpose}
Requirements: {Brief overview of requirements}
***********************************************/
//Use these comments for in-line comments or method comments
/***********************************************
Summary:
{High-level overview of code block}
Inputs:
Outputs:
Created by:
Created on:
Change Log:
Initials Date Description
***********************************************/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment