-->

How to work with Assignment and Approval Maps in A

2020-02-06 03:38发布

问题:

I need to make usage Assignment and Approval Maps. What is template for usage of maps which are constructed at page EP205000? I made research in file coderepository.xml of Acumatica, and found there EPApprovalAutomation class. I wanted to use it, but it requires among arguments usage of class which implements IAssignedMap interface. It gives another problem, because IAssignedMap interface is internal, which gives another riddle, how to use IAssignedMap interface? What are alternatives?

回答1:

This answer might be coming a bit late, but I'm sure it can be useful to others so I will share it.

New tables and fields for the database

XXSetupApproval

Add new setup table for approval settings in your module (XXSetupApproval further). You can see all required fields below, but you may need to add any additional parameters if you want to split approval for different types of entities.

CREATE TABLE XXSetupApproval
(
    CompanyID int NOT NULL,
    ApprovalID int NOT NULL identity,
    AssignmentMapID int NOT NULL,
    AssignmentNotificationID int NULL,
    CreatedByID uniqueidentifier NOT NULL,
    CreatedByScreenID char(8) NOT NULL,
    CreatedDateTime datetime NOT NULL,
    LastModifiedByID uniqueidentifier NOT NULL,
    LastModifiedByScreenID char(8) NOT NULL,
    LastModifiedDateTime datetime NOT NULL,
    Tstamp timestamp NULL,
    IsActive bit NOT NULL
)
GO

ALTER TABLE XXSetupApproval
    ADD CONSTRAINT XXSetupApproval_PK PRIMARY KEY CLUSTERED (CompanyID, ApprovalID) 
GO

XXRegister

Modify table of entity for which you need to implement approval mechanism (XXRegister further). Your entity should include three required fields that you can see below. OwnerID uniqueidentifier NULL, WorkGroupID int NULL, Approved bit NOT NULL XXSetup Modify table of main setup in your module (XXSetup further). Your setup should include one required field that you can see below. Name this flag according with your entity name, because it will be indicate whether approval mechanism enabled or not (XXRequestApproval further). XXRequestApproval bit NULL

New update scripts for the database

Update XXRegister table and set Approved flag equal to 1 for all existing records according with your conditions. Use your own expressions instead of three dots.

EXEC sp_executesql N'UPDATE XXRegister SET Approved = 1 WHERE ...'

New tables and fields for the code

AssignmentMapTypeXX

Add your entity to the AssignmentMapType class (AssignmentMapTypeXX further). This type should be used for select the assignment maps of needed types only.

public static class AssignmentMapType
{
    ...
    public class AssignmentMapTypeXX : Constant<string>
    {
        public AssignmentMapTypeXX() : base(typeof(XXRegister).FullName) { }
    }
    ...
}

XXSetup

Add new properties and classes to the XXSetup DAC according with new fields in the database. Use any other attributes if needed.

#region XXRequestApproval
public abstract class xXRequestApproval : PX.Data.IBqlField { }
[EPRequireApproval]
[PXDefault(false, PersistingCheck = PXPersistingCheck.Null)]
[PXUIField(DisplayName = "Require Approval")]
public virtual bool? XXRequestApproval { get; set; }       
#endregion
XXSetupApproval
Add new DAC to the code according with XXSetupApproval table in the database. XXSetupApproval DAC should realize IAssignedMap interface (AssignmentMapID, AssignmentNotificationID, IsActive fields). Use any other attributes if needed.
[Serializable]
public partial class XXSetupApproval : IBqlTable, IAssignedMap
{
    #region ApprovalID
    public abstract class approvalID : IBqlField { }
    [PXDBIdentity(IsKey = true)]
    public virtual int? ApprovalID { get; set; }
    #endregion

    #region AssignmentMapID
    public abstract class assignmentMapID : IBqlField { }
    [PXDefault]
    [PXDBInt]
    [PXSelector(typeof(Search<EPAssignmentMap.assignmentMapID, Where<EPAssignmentMap.entityType, Equal<AssignmentMapType.AssignmentMapTypeXX>>>), 
        DescriptionField = typeof(EPAssignmentMap.name))]
    [PXUIField(DisplayName = "Approval Map")]
    public virtual int? AssignmentMapID { get; set; }
    #endregion

    #region AssignmentNotificationID
    public abstract class assignmentNotificationID : IBqlField { }
    [PXDBInt]
    [PXSelector(typeof(PX.SM.Notification.notificationID), SubstituteKey = typeof(PX.SM.Notification.name))]
    [PXUIField(DisplayName = "Pending Approval Notification")]
    public virtual int? AssignmentNotificationID { get; set; }
    #endregion

    #region tstamp
    public abstract class Tstamp : IBqlField { }
    [PXDBTimestamp()]
    public virtual byte[] tstamp { get; set; }     
    #endregion

    #region CreatedByID
    public abstract class createdByID : IBqlField { }
    [PXDBCreatedByID()]
    public virtual Guid? CreatedByID { get; set; }     
    #endregion

    #region CreatedByScreenID
    public abstract class createdByScreenID : IBqlField { }
    [PXDBCreatedByScreenID()]
    public virtual string CreatedByScreenID { get; set; }          
    #endregion

    #region CreatedDateTime
    public abstract class createdDateTime : IBqlField { }
    [PXDBCreatedDateTime()]
    public virtual DateTime? CreatedDateTime { get; set; }         
    #endregion

    #region LastModifiedByID
    public abstract class lastModifiedByID : IBqlField { }
    [PXDBLastModifiedByID()]
    public virtual Guid? LastModifiedByID { get; set; }        
    #endregion

    #region LastModifiedByScreenID
    public abstract class lastModifiedByScreenID : IBqlField { }
    [PXDBLastModifiedByScreenID()]
    public virtual string LastModifiedByScreenID { get; set; }         
    #endregion

    #region LastModifiedDateTime
    public abstract class lastModifiedDateTime : IBqlField { }
    [PXDBLastModifiedDateTime()]
    public virtual DateTime? LastModifiedDateTime { get; set; }        
    #endregion

    #region IsActive
    public abstract class isActive : IBqlField { }
    [PXDBBool()]
    [PXDefault(typeof(Search<XXSetup.xXRequestApproval>), PersistingCheck = PXPersistingCheck.Nothing)]
    public virtual bool? IsActive { get; set; }
    #endregion
}

XXRegister

Add PXEmailSource attribute to the XXRegister DAC, it is required for "Assignment and Approvals Maps" tree selector.

[PXEMailSource]
public partial class XXRegister : IBqlTable, EP.IAssign ...

Add new properties and classes to the XXRegister DAC according with new fields in the database. XXRegister DAC should realize IAssign interface (OwnerID, WorkgroupID fields). Use any other attributes if needed. Use your own expressions instead of three dots.

#region OwnerID
public abstract class ownerID : IBqlField { }
[PXDBGuid()]
[PXDefault(typeof(...), PersistingCheck = PXPersistingCheck.Nothing)]
[PX.TM.PXOwnerSelector()]
[PXUIField(DisplayName = "Owner")]
public virtual Guid? OwnerID { get; set; }
#endregion

#region WorkgroupID
public abstract class workgroupID : IBqlField { }
[PXDBInt]
[PXDefault(typeof(...), PersistingCheck = PXPersistingCheck.Nothing)]
[PX.TM.PXCompanyTreeSelector]
[PXUIField(DisplayName = "Workgroup", Enabled = false)]
public virtual int? WorkgroupID { get; set; }      
#endregion

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

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

Statuses

Add new approval statuses for the entity and use them in the list. Use other letters if “P” and “R” already in use.

public const string PendingApproval = "P";
public const string Rejected = "R";

public class ListAttribute : PXStringListAttribute
{
    public ListAttribute() : base(
    new string[] { ..., PendingApproval, Rejected, ... },
    new string[] { ..., EP.Messages.PendingApproval, EP.Messages.Rejected, ... }) { ; }
}

New code for the graph of the entity

Implement EPApprovalAutomation helper in the graph which is manipulating with your entity. Use your own parameters instead of three dots.

public EPApprovalAutomation<...> Approval;

Add view for the XXSetupApproval DAC. Use your own expressions instead of three dots.

public PXSelect<XXSetupApproval, Where<...>> SetupApproval;

New code for the graph of the main setup

Add view for the XXSetupApproval DAC.

public PXSelect<APSetupApproval> SetupApproval;

Update each XXSetupApproval row according with new value of the XXRequestApproval field.

protected virtual void XXSetup_XXRequestApproval_FieldVerifying(PXCache sender, PXFieldVerifyingEventArgs e)
{
    PXCache cache = this.Caches[typeof(XXSetupApproval)];
    foreach (XXSetupApproval setup in PXSelect<XXSetupApproval>.Select(this))
    {
        setup.IsActive = (bool?)e.NewValue;
        cache.Update(setup);
    }
}

New tables and fields for the web pages

XXSetup.aspx

Add new tab with approval settings to the main web page of the setup (XXSetup.aspx further). Use any other parameters if needed.

<px:PXTabItem Text="Approval">             
    <Template>
        <px:PXPanel ID="panelApproval" runat="server" >
            <px:PXLayoutRule runat="server" LabelsWidth="S" ControlSize="XM" />
            <px:PXCheckBox ID="chkXXRequestApproval" runat="server" AlignLeft="True" Checked="True" DataField="XXRequestApproval" CommitChanges="True" />                    
        </px:PXPanel>
        <px:PXGrid ID="gridApproval" runat="server" DataSourceID="ds" SkinID="Details" Width="100%" >
            <AutoSize Enabled="True" />
            <Levels>
                <px:PXGridLevel DataMember="SetupApproval" >
                    <RowTemplate>
                        <px:PXLayoutRule runat="server" StartColumn="True" LabelsWidth="M" ControlSize="XM" />
                        <px:PXSelector ID="edAssignmentMapID" runat="server" DataField="AssignmentMapID" AllowEdit="True" CommitChanges="True" />
                        <px:PXSelector ID="edAssignmentNotificationID" runat="server" DataField="AssignmentNotificationID" AllowEdit="True" />
                    </RowTemplate>
                    <Columns>
                        <px:PXGridColumn DataField="AssignmentMapID" Width="250px" RenderEditorText="True" TextField="AssignmentMapID_EPAssignmentMap_Name" />
                        <px:PXGridColumn DataField="AssignmentNotificationID" Width="250px" RenderEditorText="True" />
                    </Columns>
                </px:PXGridLevel>
            </Levels>                       
        </px:PXGrid>
        </Template>
</px:PXTabItem>

XXRegister.aspx

Add new field “Approved” to the main web page of the entity (XXRegister.aspx further). Use any other parameters if needed.

<px:PXCheckBox ID="chkApproved" runat="server" DataField="Approved" CommitChanges="True" Enabled="False" />

Add new tab with approval information to the XXRegister.aspx web page. Use any other parameters if needed.

<px:PXTabItem Text="Approval Details" BindingContext="form" RepaintOnDemand="false">
    <Template>
        <px:PXGrid ID="gridApproval" runat="server" DataSourceID="ds" Width="100%" SkinID="DetailsInTab" NoteIndicator="True" Style="left: 0px; top: 0px;">
            <AutoSize Enabled="True" />
            <Mode AllowAddNew="False" AllowDelete="False" AllowUpdate="False" />
            <Levels>
                <px:PXGridLevel DataMember="Approval">
                    <Columns>
                        <px:PXGridColumn DataField="ApproverEmployee__AcctCD" Width="160px" />
                        <px:PXGridColumn DataField="ApproverEmployee__AcctName" Width="160px" />
                        <px:PXGridColumn DataField="ApprovedByEmployee__AcctCD" Width="100px" />
                        <px:PXGridColumn DataField="ApprovedByEmployee__AcctName" Width="160px" />
                        <px:PXGridColumn DataField="ApproveDate" Width="90px" />
                        <px:PXGridColumn DataField="Status" AllowNull="False" AllowUpdate="False" RenderEditorText="True"/>
                        <px:PXGridColumn DataField="WorkgroupID" Width="150px" />
                    </Columns>
                </px:PXGridLevel>
            </Levels>
        </px:PXGrid>
    </Template>
</px:PXTabItem>

New Automation steps for the Automation definition

This is the most difficult part during approval implementation because a few new automation steps should be created in the scope of current behavior of the entity. For example, the entity has next statuses and automation steps accordingly: "Hold" -> "Open". And we should implement approval mechanism between those two steps. Then three new automation steps should be created: "Hold-Open" (if we don’t need to approve the document), "Hold-Pending Approval" (if we need to approve the document), "Pending Approval" (step from which the entity should be approved or rejected). New life cycle will be looks like this: "Hold" -> "Hold-Open" OR "Hold-Pending Approval" -> "Open" OR "Pending Approval". So "Hold-Open" and "Hold-Pending Approval" automation steps are only switches, that determine, which automation step should be used after "Hold".

<Step StepID="Hold-Open" Description="Hold-Open" GraphName="…" ViewName="Document" TimeStampName="tstamp">
    <Filter FieldName="Status" Condition="Equals" Value="H" Operator="And" />
    <Filter FieldName="Hold" Condition="Equals" Value="False" Value2="False" Operator="And" />
    <Filter FieldName="Approved" Condition="Equals" Value="True" Value2="False" Operator="And" />
    <Action ActionName="*" IsDefault="1">
        <Fill FieldName="Status" Value="N" />
    </Action>
 </Step>
<Step StepID="Hold-Pending Approval" Description="Hold-Pending Approval" GraphName="…" ViewName="Document" TimeStampName="tstamp">
    <Filter OpenBrackets="1" FieldName="Status" Condition="Equals" Value="H" Operator="Or" />
    <Filter FieldName="Status" Condition="Equals" Value="N" CloseBrackets="1" Operator="And" />
    <Filter FieldName="Hold" Condition="Equals" Value="False" Value2="False" Operator="And" />
    <Filter FieldName="Approved" Condition="Equals" Value="False" Value2="False" Operator="And" />
    <Action ActionName="*" IsDefault="1">
        <Fill FieldName="Status" Value="P" />
    </Action>
</Step>

Thanks to Evgeny Kralko from the Acumatica dev team for his work.



回答2:

Adding this as an answer because it is too big for a comment. My response below builds on Gabriel's answer...

I was able to get the steps that Gabriel posted working with the following additions: (Referred to graph SOOrderEntry for examples)

  1. Create an Assignment and Approval Map record for your screen/entity
  2. Need to get a record into your XXSetupApproval table that links to the Approval map ID created above
  3. Because IAssignedMap is internal, you need to create your own XXApprovalAutomation class that inherits EPApprovalAutomation and override GetAssignedMaps. Use this on your graph in place of EPApprovalAutomation. Make sure you add this after your XXSetupApproval SetupApproval view or you will receive an error on the page similar to "The view with type 'XXSetupApproval' must be declared inside the graph for using EPApprovalAutomation".
  4. Add PXActions to your graph for "Actions" and "Hold". If you do not have the action you might receive the following error when trying to approve: "Automation for screen/graph %GraphName% exists but is not configured properly. Failed to find action - 'Action'"
  5. Add another automation step for the "Approve" or "Reject" menu items for the Actions drop menu. I followed the Standard Sales Order "SO Pending Approval" automation step as the basis for creating my custom graphs approval process automation step.
  6. If you want to fill in the EPApproval fields such as Description as seen on the approval form by the approver, you can do a cache attached in your graph containing the approval setup like so:

    [PXDBString(60, IsUnicode = true)] [PXDefault(typeof(MyDac.description), PersistingCheck = PXPersistingCheck.Nothing)] protected virtual void EPApproval_Descr_CacheAttached(PXCache sender) { }

  7. If you want the "Type" value on the approval screen to be something user friendly and not the object namespace (PX.Objects.MyStuff.MyDac) then use PXCacheName attribute on the primary DAC like so:

    [PXEMailSource] [Serializable] [PXCacheName("My DAC")] [PXPrimaryGraph(typeof(MyGraph))] public class MyDac : PX.Data.IBqlTable, PX.Data.EP.IAssign {...}

My custom approval process on my custom graph is fulling functional. Big help/thanks to Gabriel and Evgeny for the article. The version of Acumatica I am using is 5.30.2233. One limitation is not having a way to package the Automation steps so distributing this from a customization package is not flexible.



回答3:

If you check the RQRequestEntry, there's an additional code in "Hold"

if (order.Hold != true && order.Approved != true)
                {
                    order.CheckBudget = false;
                    if (order.BudgetValidation == true)
                        foreach (RQBudget budget in this.Budget.Select())
                        {
                            if (budget.RequestAmt > budget.BudgetAmt)
                            {
                                order.CheckBudget = true; break;
                            }
                        }
                    if(order.CheckBudget == true)
                    {
                        RQRequestClass cls = this.reqclass.SelectWindowed(0,1,order.ReqClassID);
                        if (cls != null && cls.BudgetValidation == RQRequestClassBudget.Error)
                            throw new PXRowPersistedException(typeof(RQRequest).Name, order, Messages.CheckBudgetWarning);
                    }

                    if (Setup.Current.RequestAssignmentMapID != null)
                    {
                        var processor = new EPAssignmentProcessor<RQRequest>();
                        processor.Assign(order, Setup.Current.RequestAssignmentMapID);
                        order.WorkgroupID = order.ApprovalWorkgroupID;
                        order.OwnerID = order.ApprovalOwnerID;
                    }
                }
                yield return (RQRequest)Document.Search<RQRequest.orderNbr>(order.OrderNbr);

I've been trying to figure out the third level approval step and the instructions doesn't include this part. I tried even debugging acumatica's code and there seem to be no errors there. For the benefit of the other developers here is what you need

you need this part of the code to assign to the next approver:

if (Setup.Current.RequestAssignmentMapID != null)
                    {
                        var processor = new EPAssignmentProcessor<RQRequest>();
                        processor.Assign(order, Setup.Current.RequestAssignmentMapID);
                        order.WorkgroupID = order.ApprovalWorkgroupID;
                        order.OwnerID = order.ApprovalOwnerID;
                    }