Finally got the complete process working, using standard Acumatica code and Automation steps. This was particularly painful for me, being new to Acumatica, and I've had to take many breaks from it as I learned other parts of the system and tools. For those that want to avoid the countless hours of trial and error that I endured, here is the relevant code for your customization in 2018R1. (I understand some things may have to be rewritten for R2, so be mindful of your version if you are having trouble making it work.)
Before you dive in, please note that I left the original answer because it was part of my learning curve and may help you relate from where I started (which might be where you are) to where I ended up with it working as desired.
MyGraph:
using PX.Data;
using PX.Objects.AP;
using PX.Objects.AR;
using PX.Objects.CR;
using PX.Objects.EP;
using PX.Objects.IN;
using System.Collections;
using System.Collections.Generic;
namespace MyNamespace
{
public class MyGraph : PXGraph<MyGraph, XXDocument>
{
[PXViewName(Messages.MyGraph)]
public PXSelect<XXDocument, Where<XXDocument.branchID, Equal<Current<AccessInfo.branchID>>>> MyView;
public PXSetup<XXSetup> MySetup;
public PXSelect<XXSetupApproval> SetupApproval;
// THIS WILL USE THE STANDARD APPROVAL CODE AND SUPPORT THE STANDARD APPROVAL SCREEN
[PXViewName(Messages.Approval)]
public EPApprovalAutomation<XXDocument, XXDocument.approved, XXDocument.rejected, XXDocument.hold, XXSetupApproval> Approval;
// RESET REQUESTAPPROVAL FIELD FROM THE SETUP SCREEN SETTING
protected virtual void XXDocument_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
XXDocument doc = e.Row as XXDocument;
if (doc == null)
{
return;
}
doc.RequestApproval = MySetup.Current.XXRequestApproval;
}
public MyGraph()
{
XXSetup setup = MySetup.Current;
}
// SETS UP THE ACTIONS MENU INCLUDING @actionID = Persist and @refresh FOR AUTOMATION STEPS
public PXAction<XXDocument> action;
[PXUIField(DisplayName = "Actions", MapEnableRights = PXCacheRights.Select)]
[PXButton]
protected virtual IEnumerable Action(PXAdapter adapter,
[PXInt] [PXIntList(new int[] { 1, 2 }, new string[] { "Persist", "Update" })] int? actionID,
[PXBool] bool refresh,
[PXString] string actionName
)
{
List<XXDocument> result = new List<XXDocument>();
if (actionName != null)
{
PXAction a = this.Actions[actionName];
if (a != null)
foreach (PXResult<XXDocument> e in a.Press(adapter))
result.Add(e);
}
else
foreach (XXDocument e in adapter.Get<XXDocument>())
result.Add(e);
if (refresh)
{
foreach (XXDocument MyView in result)
MyView.Search<XXDocument.refNbr>(MyView.RefNbr);
}
switch (actionID)
{
case 1:
Save.Press();
break;
case 2:
break;
}
return result;
}
public PXAction<XXDocument> hold;
// QUICK DEFAULT BASED ON WETHER APPROVAL SETUPS ARE DEFINED PROPERLY
protected virtual void XXDocument_Approved_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
e.NewValue = MySetup.Current == null || MySetup.Current.XXRequestApproval != true;
}
// THESE (EPApproval_XX_CacheAttached) WILL SET VALUES IN THE GRID FOR THE STANDARD APPROVAL PROCESSING SCREEN
#region EPApproval Cache Attached
[PXDBDate()]
[PXDefault(typeof(XXDocument.docDate), PersistingCheck = PXPersistingCheck.Nothing)]
protected virtual void EPApproval_DocDate_CacheAttached(PXCache sender)
{
}
[PXDBInt()]
[PXDefault(typeof(XXDocument.bAccountID), PersistingCheck = PXPersistingCheck.Nothing)]
protected virtual void EPApproval_BAccountID_CacheAttached(PXCache sender)
{
}
[PXDBString(60, IsUnicode = true)]
[PXDefault(typeof(XXDocument.description), PersistingCheck = PXPersistingCheck.Nothing)]
protected virtual void EPApproval_Descr_CacheAttached(PXCache sender)
{
}
[PXDBLong()]
[CurrencyInfo(typeof(XXDocument.curyInfoID))]
protected virtual void EPApproval_CuryInfoID_CacheAttached(PXCache sender)
{
}
[PXDBDecimal(4)]
[PXDefault(typeof(XXDocument.curyTotalAmount), PersistingCheck = PXPersistingCheck.Nothing)]
protected virtual void EPApproval_CuryTotalAmount_CacheAttached(PXCache sender)
{
}
[PXDBDecimal(4)]
[PXDefault(typeof(XXDocument.totalAmount), PersistingCheck = PXPersistingCheck.Nothing)]
protected virtual void EPApproval_TotalAmount_CacheAttached(PXCache sender)
{
}
#endregion
}
}
My Document DAC:
using PX.Data;
using PX.Data.EP;
using PX.Objects.CS;
using PX.Objects.EP;
using PX.Objects.SM;
using PX.SM;
using PX.TM;
using System;
namespace MyNamespace
{
[PXEMailSource]
[Serializable]
[PXPrimaryGraph(typeof(MyGraph))]
[PXCacheName(Messages.XXDocument)]
public partial class XXDocument : IBqlTable, IAssign
{
#region Selected
[PXBool()]
[PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
[PXUIField(DisplayName = "Selected")]
public virtual bool? Selected { get; set; }
public abstract class selected : IBqlField { }
#endregion
#region BranchID
[PXDBInt()]
[PXUIField(DisplayName = "Branch ID")]
public virtual int? BranchID { get; set; }
public abstract class branchID : IBqlField { }
#endregion
#region DocumentID
[PXDBIdentity]
public virtual int? DocumentID { get; set; }
public abstract class documentID : IBqlField { }
#endregion
#region RefNbr
[PXDBString(15, IsKey = true, IsUnicode = true, InputMask = "")]
[PXUIField(DisplayName = "Ref Nbr", Visibility = PXUIVisibility.SelectorVisible)]
[AutoNumber(typeof(XXSetup.numberingID), typeof(AccessInfo.businessDate))]
[PXSelector(typeof(XXDocument.refNbr),
typeof(XXDocument.refNbr),
typeof(XXDocument.createdDateTime)
)]
public virtual string RefNbr { get; set; }
public abstract class refNbr : IBqlField { }
#endregion
#region Hold
[PXDBBool()]
[PXUIField(DisplayName = "Hold", Visibility = PXUIVisibility.Visible)]
[PXDefault(true)]
public virtual bool? Hold { get; set; }
public abstract class hold : IBqlField { }
#endregion
#region Approved
// MAKE THIS PXDBBOOL IF YOU WANT TO SAVE THIS IN THE DATABASE LIKE POORDER.APPROVED FIELD
// NOT NECESSARY IN MY CODE BUT CAN AFFECT HOW YOU DEFINE AUTOMATION STEPS
[PXBool()]
[PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
// REMEMBER PXUIFIELD IF YOU WANT TO DISPPLAY ON THE SCREEN - I DID NOT WANT THIS ON MY SCREEN
//[PXUIField(DisplayName = "Approved", Visibility = PXUIVisibility.Visible, Enabled = false)]
public virtual Boolean? Approved { get; set; }
public abstract class approved : IBqlField { }
#endregion
#region Rejected
[PXBool]
[PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
public bool? Rejected { get; set; }
public abstract class rejected : IBqlField { }
#endregion
#region RequestApproval
[PXBool()]
[PXUIField(DisplayName = "Request Approval", Visible = false)]
public virtual bool? RequestApproval { get; set; }
public abstract class requestApproval : IBqlField { }
#endregion
#region Status
[PXDBString(1)]
[PXDefault(XXDocument.Statuses.Hold)]
[PXUIField(DisplayName = "Status", Visibility = PXUIVisibility.SelectorVisible, Enabled = false)]
[Statuses.List]
public virtual string Status { get; set; }
public abstract class status : IBqlField { }
#endregion
#region Description
[PXDBString(255, IsUnicode = true, InputMask = "")]
[PXUIField(DisplayName = "Description")]
public virtual string Description { get; set; }
public abstract class description : IBqlField { }
#endregion
// ADD A VERSION OF AMOUNT FOR CURRENCY AND ALSO A FIELD FOR CURRENCY ID IF YOU WANT IN YOUR APPROVAL SCREEN
#region Amount
[PXDBDecimal(2)]
[PXDefault(TypeCode.Decimal, "0.0")]
[PXUIField(DisplayName = "Amount", Enabled = false)]
public virtual decimal? Amount { get; set; }
public abstract class amount : IBqlField { }
#endregion
#region DocDate
[PXDBDate()]
[PXUIField(DisplayName = "Date")]
[PXDefault(typeof(AccessInfo.businessDate))]
public virtual DateTime? DocDate { get; set; }
public abstract class docDate : IBqlField { }
#endregion
#region BAccountID
/// <summary>
/// The ID of the workgroup which was assigned to approve the transaction.
/// </summary>
[PXInt]
[PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
public virtual int? BAccountID { get; set; }
public abstract class bAccountID : IBqlField { }
#endregion
#region OwnerID
[PXDBGuid()]
[PXDefault(typeof(Search<EPEmployee.userID, Where<EPEmployee.userID, Equal<Current<AccessInfo.userID>>>>), PersistingCheck = PXPersistingCheck.Nothing)]
[PX.TM.PXOwnerSelector()]
[PXUIField(DisplayName = "Owner")]
public virtual Guid? OwnerID { get; set; }
public abstract class ownerID : IBqlField { }
#endregion
#region WorkgroupID
/// <summary>
/// The ID of the workgroup which was assigned to approve the transaction.
/// </summary>
[PXInt]
[PXSelector(typeof(Search<EPCompanyTree.workGroupID>), SubstituteKey = typeof(EPCompanyTree.description))]
[PXUIField(DisplayName = "Approval Workgroup ID", Enabled = false)]
public virtual int? WorkgroupID { get; set; }
public abstract class workgroupID : IBqlField { }
#endregion
#region CreatedByID
[PXDBCreatedByID()]
public virtual Guid? CreatedByID { get; set; }
public abstract class createdByID : IBqlField { }
#endregion
#region CreatedByScreenID
[PXDBCreatedByScreenID()]
public virtual string CreatedByScreenID { get; set; }
public abstract class createdByScreenID : IBqlField { }
#endregion
#region CreatedDateTime
[PXDBCreatedDateTime()]
[PXUIField(DisplayName = "Created Date Time")]
public virtual DateTime? CreatedDateTime { get; set; }
public abstract class createdDateTime : IBqlField { }
#endregion
#region LastModifiedByID
[PXDBLastModifiedByID()]
public virtual Guid? LastModifiedByID { get; set; }
public abstract class lastModifiedByID : IBqlField { }
#endregion
#region LastModifiedByScreenID
[PXDBLastModifiedByScreenID()]
public virtual string LastModifiedByScreenID { get; set; }
public abstract class lastModifiedByScreenID : IBqlField { }
#endregion
#region LastModifiedDateTime
[PXDBLastModifiedDateTime()]
[PXUIField(DisplayName = "Last Modified Date Time")]
public virtual DateTime? LastModifiedDateTime { get; set; }
public abstract class lastModifiedDateTime : IBqlField { }
#endregion
#region Tstamp
[PXDBTimestamp()]
[PXUIField(DisplayName = "Tstamp")]
public virtual byte[] Tstamp { get; set; }
public abstract class tstamp : IBqlField { }
#endregion
#region NoteID
[PXSearchable(INSERT YOUR SEARCHABLE CODE HERE OR REMOVE THIS LINE TO NOT BE SEARCHABLE)]
[PXNote]
public virtual Guid? NoteID { get; set; }
public abstract class noteID : IBqlField { }
#endregion
#region DeletedDatabaseRecord
[PXDBBool()]
[PXDefault(false)]
[PXUIField(DisplayName = "Deleted Database Record")]
public virtual bool? DeletedDatabaseRecord { get; set; }
public abstract class deletedDatabaseRecord : IBqlField { }
#endregion
#region IAssign Members
int? PX.Data.EP.IAssign.WorkgroupID
{
get { return WorkgroupID; }
set { WorkgroupID = value; }
}
Guid? PX.Data.EP.IAssign.OwnerID
{
get { return OwnerID; }
set { OwnerID = value; }
}
#endregion
public static class Statuses
{
public class ListAttribute : PXStringListAttribute
{
public ListAttribute() : base(
new[]
{
Pair(Hold, PX.Objects.EP.Messages.Hold),
Pair(PendingApproval, PX.Objects.EP.Messages.PendingApproval),
Pair(Approved, PX.Objects.EP.Messages.Approved),
Pair(Rejected, PX.Objects.EP.Messages.Rejected),
})
{ }
}
public const string Hold = "H";
public const string PendingApproval = "P";
public const string Approved = "A";
public const string Rejected = "V"; // V = VOIDED
}
}
public static class AssignmentMapType
{
public class AssignmentMapTypeXX : Constant<string>
{
public AssignmentMapTypeXX() : base(typeof(XXDocument).FullName) { }
}
}
}
Override EPApprovalMapMaint Screen Types List:
using PX.Data;
using PX.SM;
using PX.TM;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using PX.Common;
using PX.Objects;
using PX.Objects.EP;
namespace PX.Objects.EP
{
public class EPApprovalMapMaint_Extension : PXGraphExtension<EPApprovalMapMaint>
{
#region Event Handlers
public delegate IEnumerable<String> GetEntityTypeScreensDelegate();
[PXOverride]
public IEnumerable<String> GetEntityTypeScreens(GetEntityTypeScreensDelegate baseMethod)
{
return new string[]
{
"AP301000",//Bills and Adjustments
"AP302000",//Checks and Payments
"AP304000",//Quick Checks
"AR302000",//Payments and Applications
"AR304000",//Cash Sales
"CA304000",//Cash Transactions
"EP305000",//Employee Time Card
"EP308000",//Equipment Time Card
"EP301000",//Expense Claim
"EP301020",//Expense Receipt
"PM301000",//Projects
"PM307000",//Proforma
"PM308000",//Change Order
"PO301000",//Purchase Order
"RQ301000",//Purchase Request
"RQ302000",//Purchase Requisition
"SO301000",//Sales Order
"CR304500",//Quote
"XX000000"//My Custom Document Screen
};
//return baseMethod();
}
#endregion
}
}
My Automation Steps. This is in XML generated from the Automation Definitions screen, but you would be best served to learn to read this format and duplicate the appropriate steps in your Automation Steps screen. In my case, I leverage a non-database field for approved to help trigger the advancement of approvals (EPApprovalAutomation) but my statuses go: On Hold -> Pending Approval -> [Approved|Rejected] where I don't actually need to store "approved" in the database.
<?xml version="1.0" encoding="utf-8"?>
<Screens>
<Screen ScreenID="XX000000">
<Menu ActionName="Action">
<MenuItem Text="Approve" />
<MenuItem Text="Reject" />
</Menu>
<Step StepID="Approved" GraphName="MyNamespace.MyGraph" ViewName="Savings" TimeStampName="Tstamp">
<Filter FieldName="Approved" Condition="Equals" Value="True" Value2="False" Operator="And" />
<Filter FieldName="Status" Condition="Equals" Value="P" Operator="And" />
<Action ActionName="Action" MenuText="Approve" IsDisabled="1" />
<Action ActionName="Action" MenuText="Reject" IsDisabled="1" />
<Action ActionName="*" IsDefault="1">
<Fill FieldName="Status" Value="A" />
</Action>
</Step>
<Step StepID="Hold" GraphName="MyNamespace.MyGraph" ViewName="Savings" TimeStampName="Tstamp">
<Filter FieldName="Hold" Condition="Equals" Value="True" Value2="False" Operator="And" />
<Filter FieldName="Status" Condition="Does Not Equal To" Value="H" Operator="And" />
<Action ActionName="*" IsDefault="1" AutoSave="4">
<Fill FieldName="Status" Value="H" />
</Action>
<Action ActionName="Action" MenuText="Approve" IsDisabled="1" />
<Action ActionName="Action" MenuText="Reject" IsDisabled="1" />
</Step>
<Step StepID="Hold-Pending Approval" GraphName="MyNamespace.MyGraph" ViewName="Savings" TimeStampName="Tstamp">
<Filter FieldName="Hold" Condition="Equals" Value="False" Value2="False" Operator="And" />
<Filter FieldName="Status" Condition="Equals" Value="H" Operator="And" />
<Action ActionName="*" IsDefault="1" AutoSave="4">
<Fill FieldName="Status" Value="P" />
</Action>
</Step>
<Step StepID="Pending Approval" GraphName="MyNamespace.MyGraph" ViewName="Savings" TimeStampName="Tstamp">
<Filter FieldName="Status" Condition="Equals" Value="P" Operator="And" />
<Action ActionName="Action" MenuText="Approve">
<Fill FieldName="Approved" Value="True" />
<Fill FieldName="@actionID" Value="1" />
<Fill FieldName="@refresh" Value="True" />
</Action>
<Action ActionName="Action" MenuText="Reject">
<Fill FieldName="Rejected" Value="True" />
<Fill FieldName="Status" Value="V" />
<Fill FieldName="@actionID" Value="1" />
<Fill FieldName="@refresh" Value="True" />
</Action>
</Step>
</Screen>
</Screens>
The other parts of the code, including the setup screens are easy enough to create following Brendan's post mentioned in the original post. These code samples should guide you through all the parts that I struggled with through a lot of trial and error and trying to follow breadcrumbs through the code (with a lot of the trails ending at just metadata instead of full-blown code that isn't actually in my CodeRepository).
With the answers right in front of me, this all looks pretty simple. But now knowing how the system processes approvals at a code level made it very challenging for me to find the way.
Fundamentally, understand that EPApprovalAutomation is the standard approval hook. It manages the EPApproval records which are the actual approvals. You will need to setup an Approval Map for it to understand the approval tree to follow, but again, Brendan did a great job explaining those setups in his post.
You tell EPApprovalAutomation what the document/record is that you want to approve and give it the relevant field names it will need for managing approval. I set the field for approved or rejected in the automation steps, and EPApprovalAutomation notices the field set to true to process the approval step. Note that it appears to only really manage the EPApproval records based on the inputs. It will lookup the proper approval map and either leave the approval completed or lookup the next step to get that started.
The Automation Steps must be written in a way to manipulate fields like status when the right conditions are met. In my case, approval happens when the approved flag is still set after the rest of the code runs in response to the Approve button. Since I don't save to the database, I can just look for when the field in my DAC is still set to true and then toggle my status to Approved. On Reject, just go ahead and set rejected to trigger rejecting the approval and then set the status right to Rejected on my document.
I am still fairly new to Acumatica, so there may be better ways to do some of this. If you happen to be more experienced and have some insights that may help us all improve our skills, please comment.