-->

Implementing a DAC with no persisted fields

2019-08-07 18:55发布

问题:

I am looking to implement a DAC to use in a Processing Page where the data will not get persisted to the database.

The use case for this question is to have data loaded from an outside web service in preparation to generate AR payment records. We would like to have the user enter a dedicated Processing page and then be able to use the existing processing page framework to follow through with the generation of payment.

We need the processing page to call out to a web service and request any unprocessed payments then load the data into DAC instances to display to the end user before payments are selected to be processed. We don't see any reason to persist this data to the database, so I have been trying to get this to work using only non-persisting attributes in the DAC. The only way I can get close to doing this is to have at least one field be declared using a [PXDB{type}] attribute. If I try having them all use non-persisting, I get an error message as follows "Incorrect syntax near the keyword 'FROM.'"

I am also looking for a way to load the processing grid when the graph is first instantiated. I can load the data via a button, but the result needs to load whenever the processing graph is newed up. I will likely create a distinct ticket for this as to keep this ticket focused on the non-persistent DAC.

I have given a genuine attempt to look through the documentation on any clues how to do this but I have not seen anything yet in this specific use case. I assume I need to use a class level attribute to set this but have not had any success finding what needs to be used.

[Serializable]
//[PXNonInstantiatedExtension] this looked close
                //to what we are looking for but experimenting 
                //with it did not yield desired results.
public class CtpPayment : IBqlTable
{

    #region Selected
    public abstract class selected : IBqlField{ }
    [PXBool]
    [PXUIField(DisplayName = "Selected")]
    public virtual bool? Selected { get; set; }
    #endregion

    public abstract class id : IBqlField { }
    //todo: find out what size we need 50 is just a guess.
    //[PXDBString(50, IsKey = true)] //We are able to get this to work only if 
                                    //we have at least one persisting field.
                                    //we can live with this but would prefer to 
                                    //have the whole class as non-persistent
    [PXString(50)] //having only non-persisting attributes will result in a 
                    //Incorrect syntax near the keyword 'FROM'. error.
    [PXUIField(DisplayName = "Click To Pay Id")]
    public virtual string Id { get; set; }

    public abstract  class description : IBqlField {}
    [PXString(200)]
    [PXUIField(DisplayName = "Payment Description")]
    public virtual string Description { get; set; }

    public abstract  class amount : IBqlField { }
    [PXDecimal(2)]
    [PXUIField(DisplayName = "Payment Amount")]
    public  virtual decimal? Amount { get; set; }

    public abstract class customerId : IBqlField { }

    [PXInt]
    [PXUIField(DisplayName = "Customer ID")]
    //todo: decorate this with the needed attributes to display friendly key instead of int.

    public virtual int? CustomerID { get; set; }
}


//the graph is defined as follows.

//todo: follow up and determine the most appropriate name for this graph.
public class CtpPaymentProcess : PXGraph<CtpPaymentProcess>
{
    public PXAction<CtpPayment> checkForC2PPayments;

    public PXSetup<CtpSetup> setup;

    public PXProcessing<CtpPayment> Payments;

    private CtpAcumatica _ctpAcumatica;
    public CtpAcumatica CtpAcumatica
    {
        get
        {
            if (_ctpAcumatica == null)
            {
                var graph = PXGraph.CreateInstance<PXGraph>();
                _ctpAcumatica = new CtpAcumatica(setup.Current.CtpUrl,
                    setup.Current.CtpApiKey,
                    "NoLongerNeeded", //todo: refactor this out.
                    "NoLongerNeeded", //todo: refactor this out.
                    graph);
            }
            return _ctpAcumatica;
        }
    }

    public CtpPaymentProcess()
    {
        Payments.SetProcessCaption("Process Payments");
        Payments.SetProcessAllCaption("Process All Payments");
        Payments.SetProcessDelegate<CtpPaymentProcess>(
            delegate(CtpPaymentProcess graph, CtpPayment payment)
            {
                graph.Clear();
                graph.ProcessPayment(payment, true);
            }
        );
        this.Initialized += InitializePayments;

    }

    private void InitializePayments(PXGraph sender)
    {
        //this looked like a candidate to auto populate the 
        //graph with pending payments on initializing the graph.
        //this unfortunately does not get the desired end result.
        //it works fine via the button.
        CreateNonPersistedPaymentRecords();
    }

    private void ProcessPayment(CtpPayment payment, bool massProcess)
    {
        PXTrace.WriteInformation($"Processing {payment}");
        //todo: process Payment
    }


    /// <summary>
    /// This is a temporary method with the purpose of exploring retrieval of payments and rendering 
    /// Payment and application records. this method will not exist in production code and will 
    /// be replaced with a dedicated PXProcessing page.
    /// </summary>
    [PXButton]
    [PXUIField(DisplayName = "Check for Click-to-Pay Payments")]
    protected void CheckForC2PPayments()
    {
        //todo: we need to find a way to do this
        //      without the user needing to hit
        //      this button.
        CreateNonPersistedPaymentRecords();
    }

    public QueryPaymentsResponseViewModel payments { get; internal set; }

    private void CreateNonPersistedPaymentRecords()
    {
        payments = this.CtpAcumatica.CheckForAllNewPayments(100);


        PXTrace.WriteInformation("Processing " + (payments.Payments.Count) + " payments");

        if (payments.Payments != null)
        {
            // Loop processing each payment returned from the gateway, storing the 
            // information into non persisted cache.
            foreach (var payment in payments.Payments)
            {
                if (!payment.IsMarkedRetrieved)
                {

                    createPaymentProcessRecord(payment);
                }
            }
        }
    }

    private void createPaymentProcessRecord(PaymentViewModel payment)
    {
        CtpPayment ctpPayment = new CtpPayment
        {
            Id = payment.Id,
            Description = $"CustID{payment.CustomerId} ApsTransID:{payment.ApsTransactionId} Currency{payment.Currency} {payment.AccountName} {payment.PaymentType} {payment.Date}",
            CustomerID = int.Parse(payment.CustomerId),
            Amount = payment.Amount
        };
        var r = Payments.Cache.Insert(ctpPayment) ?? Payments.Cache.Update(ctpPayment);
    }



}

回答1:

Big thanks to Samvel Petrosov and HB_Acumatica for pointing me in the right direction. What I needed to do was the following.

Create a method that returns a non-generic IEnumerable using the same name as the PXProcessing IdentifierName. for example, I used the following

    public PXProcessing<CtpPayment> Payments;

    public IEnumerable payments()
    {
        paymentsFromCtpServer = CtpAcumatica.CheckForAllNewPayments(100);

        PXTrace.WriteInformation("Processing " + (paymentsFromCtpServer.Payments.Count) + " paymentsFromCtpServer");

        if (paymentsFromCtpServer.Payments != null)
        {
            // Loop processing each payment returned from the gateway, storing the 
            // information into non persisted cache.
            foreach (var payment in paymentsFromCtpServer.Payments)
            {
                if (!payment.IsMarkedRetrieved)
                {

                    yield return new CtpPayment
                    {
                        CustomerID = int.Parse(payment.CustomerId),
                        Amount = payment.Amount,
                        Description = $"Payment:{payment.Id}",
                        Id = payment.Id
                    };
                }
            }
        }
    }

This also addressed my requirement to have this load with new data as soon as the page is initialized.



回答2:

Best way to utilize a DAC when you do not want to persist the data is to use a Projection as shown below, setting Persistent to false prevents saving the data.

[PXProjection(typeof(Select<MyOriginalTable, 
        Where<MyOriginalTable.myField1, Equal<True>>>), Persistent = false)]
public class MyOriginalTableProjection : PX.Data.IBqlTable
{
    public abstract class myField1: PX.Data.IBqlField
    {
    }
    protected bool? _MyField1;
    [PXDBBool(BqlField = typeof(MyOriginalTable.myField1)]
    [PXDefault(true)]
    public virtual bool? MyField1
    {
        get
        {
            return this._MyField1;
        }
        set
        {
            this._MyField1 = value;
        }
    }
    public abstract class myField2 : PX.Data.IBqlField
    {
    }
    protected String _MyField2;
    [PXDBString(1, IsKey = true, BqlField = typeof(MyOriginalTable.myField2))]
    public virtual String MyField2
    {
        get
        {
            return this._MyField2;
        }
        set
        {
            this._MyField2 = value;
        }
    }

    public abstract class myField3: PX.Data.IBqlField
    {
    }
    protected String _MyField3;
    [PXDBString(15, IsKey = true, BqlField = typeof(MyOriginalTable.myField3))]
    public virtual String MyField3
    {
        get
        {
            return this._MyField3;
        }
        set
        {
            this._MyField3 = value;
        }
    }
}


标签: acumatica