Brian Stevens
Brian Stevens

Reputation: 1951

Adding Standard Approvals to Custom Acumatica ERP Module

I have a custom module for Acumatica ERP that requires approvals. I'd like to leverage the standard approval mechanism in Acumatica ERP, so I followed this guide...

How to work with Assignment and Approval Maps in Acumatica via Automation Steps? - Answers from Gabriel and Brendan

I think everything is in place, but nothing happens when I take my record off of hold except that the event I wrote toggles the status to Pending Approval as I intended. (Maybe I should let the approval process handle that part?) I expect that my automation step should be simple because my status ALWAYS flows as:

Hold -> Pending Approval -> Approved -or- Rejected

I'm not getting any errors, so I'm not sure if my automation step is not defined right or if I am supposed to code something specific into the XXApprovalAutomation class that Brendan says to build in step 3 of his answer on the above post.

I'm still quite new to Acumatica, so a lot of the inner workings are still a bit of a mystery requiring that I dig through the CodeRepository constantly. I'm not sure if my custom XXApprovalAutomation is supposed to have something specific in the GetAssignedMaps override, but I'll keep digging and applying trial and error unless someone here can guide me.

Could someone please explain the basic process flow of how the approval system works and any relevant sample code not shown in the noted post that I need (like perhaps content of GetAssignedMaps to hit my defined approval map)? Or even better, point me to where I can read up more on it?

Upvotes: 0

Views: 2044

Answers (2)

Brian Stevens
Brian Stevens

Reputation: 1951

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.

Upvotes: 0

Brian Stevens
Brian Stevens

Reputation: 1951

Custom approvals appear have a couple of approaches, but this will stick to Automation Steps. Before proceeding, you should familiarize yourself with:

Automation Definitions - This area of Acumatica ERP 2018r1 allows you to see the automation steps all in one place for a given area of the system. For instance, selecting PO Default definition id and then selecting the Purchase Orders screen allows you to see all the automation steps defined in XML format by clicking the Show Populated button. This tool was critical for me as it pointed out important missed steps in my in my Automation Steps. This area of the system also allows you to export the automation steps you will need to migrate and bundle them with the code sent for deployment on the next system (i.e. TST -> QA -> PRD).

Automation Steps - This area of the system allows you to define "when this happens, do that" type of actions. For instance, one of my automation steps is that when the hold button is unchecked and the form is currently in On Hold status, toggle the status to Pending Approval. Be sure to explore and understand the Actions tab and use of "Fill with Values" (for setting a field value) as well as use of Action Name in the grid for defining buttons on the Action menu by selecting "Action" and then naming the button under Menu Text. This is where I defined the buttons for Approve and Reject.

Approval and Assignment Maps - This area, as HB_Acumatica was kind to point out, has documentation to explain how to use it. Read up on that, and practice with something like PO's to make sure you understand the Application side of using approvals before trying to make your own customization.

While I am still trying to get a solid grasp of what it took to make it work, the following seem to be key contributors:

Define these in your graph where you want the approval. Customizing EPApprovalAutomation as Brendan had suggested in his post appeared to be unnecessary in the end, at least for me. Might be a reason to do it, but I never found out what to customize or why.

[PXViewName("My DAC Name")]
public PXSelect<XXRegister, Where<XXRegister.branchID, Equal<Current<AccessInfo.branchID>>>> MyView;

public PXSetup<XXSetup> Setup;

public PXSelect<XXSetupApproval> SetupApproval;

[PXViewName(Messages.Approval)]
public EPApprovalAutomation<XXRegister, XXRegister.approved, XXRegister.rejected, XXRegister.hold, XXSetupApproval> Approval;

MyView can be whatever you want to call your view. It will contain your custom data. Setup is the view to the preferences DAC where you tell the system to enable Approvals for your custom data. SetupApproval is the view to the approval maps defined in your custom preferences screen when you enable approvals. (This will connect Approval Maps to Notifications so that the defined person(s) get the specified notification for approval.) Approval is THE MAGIC... This is some super secret special sauce that apparently is not for the general public to understand. However, the arguments passed in define (1) What DAC describes that data record you want to approve, (2) what field in the DAC relates to Approved, Rejected, and Hold, and (3) where should this magic box look for the approval map and notification - i.e. the name of the XXSetupApproval DAC.

Not proven fact... just what I observed... This super secret special magic box called EPApprovalAutomation appears to look at the state of the Hold field and when unchecked use the XXSetupApproval data to navigate the Approval Map. When the Actions of Approve and Reject are selected, this same magic box seems to apply the appropriate values to the approved and rejected fields. Automation Steps monitor for changes to the fields (however you have defined it) and applied your automation, such as setting the status field to Approved or Rejected. The last thing the EPApprovalAutomation seems to do is to apply the "Approved by" user, date, and status to the EPApproval record that you should be displaying in an Approval Detail tab on your custom screen.

Now, back from speculating and into things I did to get moved further along...

In the DAC for the data requiring custom approval, be sure to define Status, Hold, Approved, and Rejected as follows. Notice the difference between when it is in the database and when it is NOT.

    #region Hold
    [PXDBBool()]
    [PXUIField(DisplayName = "Hold", Visibility = PXUIVisibility.Visible)]
    [PXDefault(true)]
    //[PXNoUpdate] <- Saw this in the PO code, but had to remove so user could save stat of the Hold checkbox
    public virtual bool? Hold { get; set; }
    public abstract class hold : IBqlField { }
    #endregion

    #region Approved
    [PXDBBool()]
    [PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
    [PXUIField(DisplayName = "Approved", Visibility = PXUIVisibility.Visible, Enabled = false)]
    public virtual bool? Approved { get; set; }
    public abstract class approved : IBqlField { }
    #endregion

    #region Rejected
    [PXBool]
    [PXDefault(false, PersistingCheck = PXPersistingCheck.Nothing)]
    public abstract class rejected : IBqlField { }
    #endregion

    #region Status
    [PXDBString(1)]
    [PXDefault(XXRegister.Statuses.Hold)]
    [PXUIField(DisplayName = "Status", Visibility = PXUIVisibility.SelectorVisible, Enabled = false)]
    [Statuses.List]
    public virtual string Status { get; set; }
    public abstract class status : IBqlField { }
    #endregion

Notice that Rejected is PXBool and NOT PXDBBool - so don't add it to your table in the database.

In the graph, add your event handlers. I use this one to set the branch of the current record as I want the end user to view only records in their branch.

    #region XXRegister_RowInserting
    protected void XXRegister_RowInserting(PXCache sender, PXRowInsertingEventArgs e)
    {
        XXRegister row = (XXRegister)e.Row;
        row.BranchID = PXAccess.GetBranchID();
    }
    #endregion

I use this to programmatically toggle the status and various fields, although Acumatica ERP has this handled in Automation Steps - was simply easier for me to do this way in my current level of knowledge.

    #region XXRegister _RowUpdating
    protected void XXRegister _RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
    {
        XXRegister row = (XXRegister )e.Row;
        XXRegister newRow = (XXRegister )e.NewRow;

        if (row.Hold != newRow.Hold)
        {
            if (newRow.Hold.Equals(true))
            {
                newRow.Status = XXRegister.Statuses.Hold;
                newRow.Approved = false;
            }
            else if (row.Status.Equals(XXRegister.Statuses.Hold))
            {
                newRow.Status = XXRegister.Statuses.PendingApproval;
            }
        }
    }
    #endregion

The rest was in trying to get the Automation Steps to mimic the PO or SO. I'm not done yet, but this gets me past the hurdle that I was asking about. In full disclosure - When I Reject approval, my approval details are not being updated to display who/when/status, but my data record is updated properly. But that's not really what I was asking here, so this should cover the intended original questions.

Upvotes: 1

Related Questions