BPM To Send Email To Approver When Limit Is Exceeded On PO Approval

Hello all,

I need to create a BPM to send an email to the next Approver when a user approves a PO and the PO limit is exceeded. I’ve seen a few forums regarding this, and one that set me on a good path except the code was Progress code. I converted it to C# but am getting errors.

I’m using the PO.Update method. Has anyone found a way to something like this with E10? Thanks for your time and any help you can provide.

You may find it’s easiest to base this on the POApvMsg table.

2 Likes

Thanks for the reply, Jeff. Looking at that table, I’m not sure what the flag would be to trigger a BPM. Below is what I have now, base on the details in my first post.

There’s a Pre-Processing setup like this:

And a Post Processing setup like this:

The code in the Custom Code is:

string vapprov = string.Empty;
string vbuyer = string.Empty;
Erp.Tables.PurAgent PurAgent;

foreach (var ttPOHeader_iterator in (from ttPOHeader_Row in ttPOHeader where string.Compare(ttPOHeader_Row.ApprovalStatus ,“P” ,true)==0
select ttPOHeader_Row))
{
var ttPOHeaderRow = ttPOHeader_iterator;

if (ttPOHeader_xRow != null)
{
    PurAgent = (from PurAgent_Row in Db.PurAgent
                where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
                == ttPOHeaderRow.BuyerID
                select PurAgent_Row).FirstOrDefault();
    vapprov = PurAgent.ApprovalPerson;
    PurAgent = (from PurAgent_Row in Db.PurAgent
                where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
                == vapprov
                select PurAgent_Row).FirstOrDefault();
    vapprov = PurAgent.EMailAddress;
    PurAgent = (from PurAgent_Row in Db.PurAgent
                where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
                == ttPOHeaderRow.BuyerID
                select PurAgent_Row).FirstOrDefault();
    vbuyer = PurAgent.EmailAddress;
    if (!String.IsNullOrEmpty(vapprov))
    {
        string vFrom = string.Empty;
        string vTo = string.Empty;
        string vCC = string.Empty;
        string vSubject = string.Empty;
        string vBody = string.Empty;
        Services.Bpm.BpmEmail hEmailEx = null;
        hEmailEx = new Services.Bpm.BpmEmail();
        vFrom = vbuyer;
        vTo = vapprov;
        vCC = "";
        vSubject = vSubject + "Purchase Order " + System.Convert.ToString(ttPOHeaderRow.PONUM) + " needs " + "approval";
        vBody = vBody + "The above purchase order number requires approval";
        hEmailEx.SendEmail(false, Session.CompanyID, vFrom, vTo, vCC, vSubject, vBody, "");
        if (hEmailEx != null)
        {
            hEmailEx.Dispose();
        }
        break;
    }
}

}

And here is the error that I get when trying to save the BPM:

Server Side Exception

There is at least one compilation error.

Exception caught in: Epicor.ServiceModel

Error Detail

Description: There is at least one compilation error.
Details:
Error CS0103: The name ‘ttPOHeader_xRow’ does not exist in the current context [Update.Post.EmailApproverOnL.cs(102,9)]
Error CS1061: ‘Erp.Tables.PurAgent’ does not contain a definition for ‘EmailAddress’ and no extension method ‘EmailAddress’ accepting a first argument of type ‘Erp.Tables.PurAgent’ could be found (are you missing a using directive or an assembly reference?) [Update.Post.EmailApproverOnL.cs(120,27)]
Error CS0246: The type or namespace name ‘Services’ could not be found (are you missing a using directive or an assembly reference?) [Update.Post.EmailApproverOnL.cs(129,13)]
Error CS0246: The type or namespace name ‘Services’ could not be found (are you missing a using directive or an assembly reference?) [Update.Post.EmailApproverOnL.cs(130,28)]
Error CS1061: ‘Erp.Tablesets.POHeaderRow’ does not contain a definition for ‘PONUM’ and no extension method ‘PONUM’ accepting a first argument of type ‘Erp.Tablesets.POHeaderRow’ could be found (are you missing a using directive or an assembly reference?) [Update.Post.EmailApproverOnL.cs(134,93)]
Program: Epicor.Customization.dll
Method: PrepareException
Line Number: 99
Column Number: 13
Server Trace Stack: at Epicor.Customization.Standard.CustomizationCompiler.PrepareException(CompilerErrorCollection errors) in c:_Releases\ICE\3.1.400.19\source\Framework\Epicor.Customization\Standard\CustomizationCompiler.cs:line 99
at Epicor.Customization.Standard.CustomizationCompiler.Compile(BuildEnvironment input, String outputAssembly) in c:_Releases\ICE\3.1.400.19\source\Framework\Epicor.Customization\Standard\CustomizationCompiler.cs:line 62
at Epicor.Customization.Standard.CustomizationBuilder.Process(CustomizationProject project) in c:_Releases\ICE\3.1.400.19\source\Framework\Epicor.Customization\Standard\CustomizationBuilder.cs:line 78
at Ice.Services.BO.BpMethodSvc.AfterUpdate() in c:_Releases\ICE\3.1.400.19\source\Server\Services\BO\BpMethod\BpMethod.Events.cs:line 89
at Ice.Services.Trace.TablesetProfilingCollector.DoTablesetEventTrace(String tablesetName, String methodName, Action action) in c:_Releases\ICE\3.1.400.19\source\Framework\Epicor.Ice\Services\TablesetProfilingCollector.cs:line 198
at Ice.TablesetBound3.InnerUpdate(IceDataContext dataContext, TFullTableset tableset) in c:\_Releases\ICE\3.1.400.19\source\Framework\Epicor.Ice\Services\TablesetBound.cs:line 813 at Ice.Services.BO.BpMethodSvc.Update(BpMethodTableset& ds) in c:\_Releases\ICE\3.1.400.19\source\Server\Services\BO\BpMethod\BpMethod.Designer.cs:line 837 at Ice.Services.BO.BpMethodSvcFacade.Update(BpMethodTableset& ds) in c:\_Releases\ICE\3.1.400.19\source\Server\Services\BO\BpMethod\BpMethodSvcFacade.cs:line 224 at SyncInvokeUpdate(Object , Object[] , Object[] ) at System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs) at Epicor.Hosting.OperationBoundInvoker.InnerInvoke(Object instance, Func2 func) in c:_Releases\ICE\3.1.400.19\source\Framework\Epicor.System\Hosting\OperationBoundInvoker.cs:line 59
at Epicor.Hosting.OperationBoundInvoker.Invoke(Object instance, Func2 func) in c:\_Releases\ICE\3.1.400.19\source\Framework\Epicor.System\Hosting\OperationBoundInvoker.cs:line 28 at Epicor.Hosting.Wcf.EpiOperationInvoker.Invoke(Object instance, Object[] inputs, Object[]& outputs) in c:\_Releases\ICE\3.1.400.19\source\Framework\Epicor.System\Hosting\Wcf\EpiOperationInvoker.cs:line 23 at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc) at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc& rpc) at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet) at System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(RequestContext request, Boolean cleanThread, OperationContext currentOperationContext) at System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(RequestContext request, OperationContext currentOperationContext) at System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(IAsyncResult result) at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result) at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously) at System.ServiceModel.Channels.SecurityChannelListener1.ReceiveItemAndVerifySecurityAsyncResult`2.InnerTryReceiveCompletedCallback(IAsyncResult result)
at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.Channels.TransportDuplexSessionChannel.TryReceiveAsyncResult.OnReceive(IAsyncResult result)
at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.Channels.SynchronizedMessageSource.ReceiveAsyncResult.OnReceiveComplete(Object state)
at System.ServiceModel.Channels.SessionConnectionReader.OnAsyncReadComplete(Object state)
at System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(IAsyncResult result)
at System.Net.LazyAsyncResult.Complete(IntPtr userToken)
at System.Net.Security.NegotiateStream.ProcessFrameBody(Int32 readBytes, Byte buffer, Int32 offset, Int32 count, AsyncProtocolRequest asyncRequest)
at System.Net.Security.NegotiateStream.ReadCallback(AsyncProtocolRequest asyncRequest)
at System.Net.FixedSizeReader.CheckCompletionBeforeNextRead(Int32 bytes)
at System.Net.FixedSizeReader.ReadCallback(IAsyncResult transportResult)
at System.Runtime.AsyncResult.Complete(Boolean completedSynchronously)
at System.ServiceModel.Channels.ConnectionStream.IOAsyncResult.OnAsyncIOComplete(Object state)
at System.ServiceModel.Channels.SocketConnection.OnReceiveAsync(Object sender, SocketAsyncEventArgs eventArgs)
at System.Net.Sockets.SocketAsyncEventArgs.FinishOperationSuccess(SocketError socketError, Int32 bytesTransferred, SocketFlags flags)
at System.Net.Sockets.SocketAsyncEventArgs.CompletionPortCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* nativeOverlapped)
at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP)

Client Stack Trace

at Epicor.ServiceModel.Channels.ImplBase`1.ShouldRethrowNonRetryableException(Exception ex, DataSet dataSets)
at Ice.Proxy.BO.BpMethodImpl.Update(BpMethodDataSet ds)
at Ice.Adapters.BpMethodAdapter.OnUpdate()
at Ice.Lib.Framework.EpiBaseAdapter.Update()
at Ice.UI.App.BpMethodEntry.Transactions.MainTransactionBase.adapterUpdate()

I figured out how to make it work. Thanks everyone!

I am trying to get something like this to work and am getting the same error. How did you get it to work ?

Just another take on how to do this, we only used C# code for looking up the Purchase Agent’s email addresses and assigning it to a BPM context for use later This is a standard Data Directive on the POApvMsg table:

Erp.Tables.PurAgent PurAgent;
foreach (var ttPOApvMsg_iterator in (from ttPOApvMsg_Row in ttPOApvMsg
                                    where string.Equals(ttPOApvMsg_Row.RowMod, IceRow.ROWSTATE_ADDED, StringComparison.OrdinalIgnoreCase) || string.Equals(ttPOApvMsg_Row.RowMod, IceRow.ROWSTATE_UPDATED, StringComparison.OrdinalIgnoreCase)
    select ttPOApvMsg_Row))
{
    var ttPOApvMsgRow = ttPOApvMsg_iterator;
    foreach (var PurAgent_iterator in (from PurAgent_Row in Db.PurAgent
    	where string.Compare(PurAgent_Row.Company, ttPOApvMsgRow.Company, true) == 0
      && string.Compare(PurAgent_Row.BuyerID, ttPOApvMsgRow.MsgTo, true) == 0
    select PurAgent_Row))
    {
        PurAgent = PurAgent_iterator;			
				callContextBpmData.ShortChar01 = PurAgent.EMailAddress;				
    }
    foreach (var PurAgent_iterator in (from PurAgent_Row in Db.PurAgent
    	where string.Compare(PurAgent_Row.Company, ttPOApvMsgRow.Company, true) == 0
      && string.Compare(PurAgent_Row.BuyerID, ttPOApvMsgRow.MsgFrom, true) == 0
      select PurAgent_Row))
    {
        PurAgent = PurAgent_iterator;
				callContextBpmData.ShortChar02 = PurAgent.EMailAddress;
    }
}

6 Likes

I set up a pre-processing and post-processing directive…

For the pre-processing directive, I set up a condition to check the approval status, then enable the post directive…

For the post-processing directive, I set a condition to look for the enable call…

Then created custom code to process everything else…

Erp.Tables.PurAgent PurAgent;
string vapprov = string.Empty;

foreach (var ttPOHeader_iterator in (from ttPOHeader_Row in ttPOHeader where string.Compare(ttPOHeader_Row.ApprovalStatus ,“P” ,true)==0
select ttPOHeader_Row))
{

var ttPOHeaderRow = ttPOHeader_iterator;
if (ttPOHeader_iterator != null)
{
PurAgent = (from PurAgent_Row in Db.PurAgent
where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
== ttPOHeaderRow.BuyerID
select PurAgent_Row).FirstOrDefault();
vapprov = PurAgent.ApprovalPerson;

    PurAgent = (from PurAgent_Row in Db.PurAgent
                where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
                == vapprov
                select PurAgent_Row).FirstOrDefault();
    vapprov = PurAgent.EMailAddress;
    PurAgent = (from PurAgent_Row in Db.PurAgent
                where PurAgent_Row.Company == ttPOHeaderRow.Company && PurAgent_Row.BuyerID
                == ttPOHeaderRow.BuyerID
                select PurAgent_Row).FirstOrDefault();
    if (!String.IsNullOrEmpty(vapprov))
    {
        string vFrom = string.Empty;
        string vTo = string.Empty;
        string vCC = string.Empty;
        string vSubject = string.Empty;
        string vBody = string.Empty;
  				vFrom = "";
        vTo = vapprov;
        vCC = "";
  				vSubject = vSubject + "Purchase Order " + System.Convert.ToString(ttPOHeaderRow.PONum) + " needs approval";
        vBody = vBody + "The above purchase order number requires approval in Epicor.";
  				var mailer = this.GetMailer(async: false); 
  				var message = new Ice.Mail.SmtpMail(); 
  				message.SetTo(vTo); 
  				message.SetFrom(vFrom); 
  				message.SetCC(vCC); 
  				message.SetSubject(vSubject); 
  				message.SetBody(vBody); 
  				mailer.Send(message); 
        break;
    }
}

}

Thanks, I will dig into it tomorrow, I was trying to do all of it through a method directive.

I know this is an old post and @Rick_Bird had like this in a reply to a question I had. I am currently working on setting up the BPM Alerts for PO Approval (needed) and then once approved. It works but I am having issues with how the email is being sent.

The PO Approval that you provided works great but I get multiple emails.
I have set my self to get a CC to see what was sent but I get 2 emails. The PO is coming from another user.

Even after I approve or reject the PO I get the same email from this BPM saying I have a PO to approve…I just approved it.

Am I missing something.

In the examples I provided in this post, there are TWO BPM’s one for ‘PO Needs Review’ and another ‘PO has been reviewed’
The first is designed to notify the Approver and the second to notify the PO buyer.
The MsgType field is being evaluated to determine if the Message is for ‘Needs Review’ or ‘Has been reviewed.’ The Data Dictionary provides good documentation on this field.
If I would redesign this in E10 I would do just one BPM with two branches for each value (1 & 2) of the MsgType field, I would also drop all the C# and use variables.
I have 3 clients using different versions I created for Epicor 9, Epicor 10.0, 10.1 & 10.2 and I haven’t had any complaints, but each also have slightly different needs. For instance one needed the requester to be notified in both emails. If I ever have some time to re-write for 10.2, I will post it here.

1 Like

I eventually need both but I was just starting with notifying the approver and it is hit and miss. I will post what I have in new topic…maybe someone will see my mistake. Thanks

Hey @Kimberley, please take a look at this post

It should be everything you need.

1 Like

How are your calling the email address? Am I over looking it in you code?

On you Email alert what do you have set to From and To?

When I pasted your code in and checked it I get a lot of errors…

PurAgent =(from PurAgent_Row in Db.PurAgent
where PurAgent_Row.Company == Session.CompanyID && string.Compare(PurAgent_Row.BuyerID,ttPOApvMsg_xRow.MsgTo,true)== 0
select PurAgent_Row).FirstOrDefault();

AppPerson = PurAgent.EmailAddress;

This is looking at the MsgTo (POApvMsg Table) ID and finding the PurAgent ID (Buyer Maintenance) email address based on matching the IDs.

As for calling it in the Email function you have to bring in callContextBpmData.Character01 in the To: field

Please verify when ever copying code that you look it over. For example, quotes from a website might be a different character than what is accepted in code.

1 Like

Hi Rick,

I know this is an older post, but I like your approach and I don’t see the custom code icon in the standard data directive design menu. I am using 10.2.300. Any assistance would be appreciated.

You should see it in the top of the Caller Section:
image
But I thought some Clould customers do not have access to Execute Custom Code.
Using custom code is often popular, but it is not necessarily the best. The longer I’ve used Epicor the more I try to not use Custom Code as it is limited in some environments and Custom Code is often very suspetible during upgrades.
Usually you can often use other widgets to accomplish the same thing.
I have since dropped the Custom Code and I use the Set Argument or Fill Table From Query to get the Buyer email addresses and such for the Email.

Thanks Rick for the response. The icon is not showing, but thanks for the advice on using another widget.

I am new to Epicor. I was brought in a few weeks ago to fix some integration issues with another product. There is a lot to learn. Thanks again!

Update, if the user is not BPM Advanced User, Execute Custom Code will not display.

2 Likes