How to make selector for CSAnswers

Hello everybody,

recently one of the colleagues asked me how to make selector from Attributes values. Also that request seem trivial, but still took some time, especially with usage of FBQL query to build. 

Below goes template you may use if you'll need some kind of selector for attributes by some predefined value:

 

public class SOOrderExt : PXCacheExtension<SOOrder>
    {
        public class Codes
        {
            public const string MediaCode = "MEDIACODE";

            public class mEdiaCode : BqlType<IBqlStringstring>.Constant<mEdiaCode>
            {
                public mEdiaCode() : base("mEdiaCode")
                {
                }
            }

            public const string OrdOrigin = "ORDORIGIN";

            public class ordOrigin : BqlType<IBqlStringstring>.Constant<ordOrigin>
            {
                public ordOrigin() : base("ORDORIGIN")
                {
                }
            }
        }

        [PXSelector(typeof(SearchFor<CSAnswers.value>.Where<CSAnswers.attributeID.IsEqual<Codes.ordOrigin>>))]
        [PXDBString(50)]
        public string SomeValue1 { getset; }

        [PXSelector(typeof(SearchFor<CSAnswers.value>.Where<CSAnswers.attributeID.IsEqual<Codes.mEdiaCode>>))]
        [PXDBString(50)]
        public string SomeValue { getset; }
    }

 

 For me it was also interesting to note, that in the past it was common to use for Selector combination of PXSelector with Search, but in FBQL you'll need PXSelector with SearchFor.

How to manage default order of Columns in selector of Acumatica

Hello everybody,

today I want to leave a short notice on how to manage order of columns in Selector of Acumatica. Quite often customers has desire to manage default order of columns in selector. That is very easy to achieve with new Type[] parameter added to your selector. Take a look on how I've managed list of columns for Carrier with help of new Type[]:

[PXDBString(15, InputMask = ">aaaaaaaaaaaaaaa", IsKey = true, IsUnicode = true)]
[PXDefault]
[PXUIField(DisplayName = "Ship Via", Visibility = PXUIVisibility.SelectorVisible)]
[PXSelector(typeof(Search<Carrier.carrierID>), new Type[]
       {
         typeof(Carrier.description), typeof(CarrierExt.usrDeliveryLength), typeof(Carrier.confirmationRequired),
         typeof(Carrier.packageRequired), typeof(Carrier.isCommonCarrier)
       },
        CacheGlobal = true, DescriptionField = typeof(Carrier.description))]
protected void Carrier_CarrierID_CacheAttached(PXCache sender)
       {
       }
    }

And below goes screenshot of selector:

As you can see at screenshot, column Delivery duration appeared second in the list, but by default, if I've added that column later, it would be displayed in the end of list.

PXDimensionSelector in Acumatica

Hello everybody,

Today I want to describe PXDimensionSelector attribute usage in Acumatica. According to manual PXDimensionSelector attribute has following purpose:

Defines an input control that combines the functionality of the PXDimenstion attribute and the PXSelector attribute. A user can view the data set defined by the attribute and select a data record from this data set to assign its segmented key value to the field or to replace it with the surrogate key

After reading such purpose, I've decided to read purpose of PXDimension attribute:

Defines an input control that formats the input as a segmented key value and displays the list of allowed values for each key segment.

If those two descriptionss made you understand their pupose and way of usage, then congratulations, but for me it took muuuuuuuch longer. 

Business case ( some details intentionally changed )

I was asked to create table, which has some information about Accounts and their belonging to branches ( later I'll provide sql for table creation as well as filling of created table it with some data ). Then page Sales orders has column Branch, Account, SubAccount should filter list of Accounts and SubAccounts as you can see on the screenshot:

requirement is the following: values at column Account should be filtered by what is selected in the Column Branch by means of showing only those Accounts, which has the same branch in table CompanyAccount.

Preparation

In order to give you a better idea how data was looking like, I've created some sample data with DAC class. Also in order to have some data to play around, I've installed Acumatica with SalesDemo data in order to avoid headache of configuring all from scratch.

Below goes SQL for table creation and filling with some data.

Table creation sql:

SET ANSI_NULLS ON
GO
 
SET QUOTED_IDENTIFIER ON
GO
 
CREATE TABLE [dbo].[CompanyAccount](
	[CompanyID] [int] NOT NULL,
	[CmpBranchID] [int] NOT NULL,
	[AccountID] [int] NOT NULL,
	[Description] [nvarchar](60) NULL,
	[tstamp] [timestamp] NOT 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,
	[NoteID] [uniqueidentifier] NULL,
 CONSTRAINT [PK_CompanyAccount] PRIMARY KEY CLUSTERED 
(
	[CompanyID] ASC,
	[CmpBranchID] ASC,
	[AccountID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
 
ALTER TABLE [dbo].[CompanyAccount] ADD  CONSTRAINT [DF_CompanyAccount_CompanyID]  DEFAULT ((0)) FOR [CompanyID]
GO
 
ALTER TABLE [dbo].[CompanyAccount] ADD  CONSTRAINT [DF_Table_1_CNBranchID]  DEFAULT ((0)) FOR [CmpBranchID]
GO

Filling with data of CompanyAccount table:

INSERT INTO [dbo].[CompanyAccount] ([CompanyID],[CmpBranchID],[AccountID],[Description],[CreatedByID],[CreatedByScreenID],[CreatedDateTime]
		,[LastModifiedByID],[LastModifiedByScreenID],[LastModifiedDateTime],[NoteID])
     VALUES (
				2, 16,1195, 'Discount Taken', 'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', 
				'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', NULL)
 
INSERT INTO [dbo].[CompanyAccount] ([CompanyID],[CmpBranchID],[AccountID],[Description],[CreatedByID],[CreatedByScreenID],[CreatedDateTime]
		,[LastModifiedByID],[LastModifiedByScreenID],[LastModifiedDateTime],[NoteID])
     VALUES
           (
				2,16,1223,'Office Expense', 'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', 
				'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', NULL
		   )
INSERT INTO [dbo].[CompanyAccount] ([CompanyID],[CmpBranchID],[AccountID],[Description],[CreatedByID],[CreatedByScreenID],[CreatedDateTime]
		,[LastModifiedByID],[LastModifiedByScreenID],[LastModifiedDateTime],[NoteID])
     VALUES
           (
				2,16,1224,'Postage and Delivery', 'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', 
				'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', NULL
		   )
INSERT INTO [dbo].[CompanyAccount] ([CompanyID],[CmpBranchID],[AccountID],[Description],[CreatedByID],[CreatedByScreenID],[CreatedDateTime]
		,[LastModifiedByID],[LastModifiedByScreenID],[LastModifiedDateTime],[NoteID])
     VALUES
           (
				2,21,1166,'Computer & Office Equipment', 'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', 
				'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', NULL
		   )
INSERT INTO [dbo].[CompanyAccount] ([CompanyID],[CmpBranchID],[AccountID],[Description],[CreatedByID],[CreatedByScreenID],[CreatedDateTime]
		,[LastModifiedByID],[LastModifiedByScreenID],[LastModifiedDateTime],[NoteID])
     VALUES
           (
				2,21,1167,'Machinery & Equipment', 'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', 
				'B5344897-037E-4D58-B5C3-1BDFD0F47BF9', 'AR301000', '2018-12-13 00:52:31.550', NULL
		   )

As you can see from the sql, Branch with id 16

 

DAC class for CompanyTable:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PX.Data;
using PX.Objects.GL;
 
namespace AcumaticaSamples
{
    [Serializable]
    public class CompanyAccount : IBqlTable
    {
        #region NoteID
 
        public abstract class noteID : PX.Data.IBqlField
        {
        }
 
        [PXNote]
        public virtual Guid? NoteID { getset; }
 
        #endregion
 
        #region BranchID
        public abstract class cmpBranchID : IBqlField
        {
        }
 
        [PXDBInt(IsKey = true)]
        [PXDefault]
        [PXUIField(DisplayName = "Branch ID")]
        public virtual int? CmpBranchID { getset; }
 
        #endregion
 
        #region AccountID
        public abstract class accountID : IBqlField
        {
        }
 
        [PXDBInt(IsKey = true)]
        [PXDefault]
        [PXUIField(DisplayName = "Account", IsReadOnly = true)]
        [PXSelector(typeof(Search<Account.accountID>), SubstituteKey = typeof(Account.accountCD))]
        public virtual int? AccountID { getset; }
 
        #endregion
 
 
        #region Description
        public abstract class description : IBqlField
        {
        }
        [PXDBString(60)]
        [PXUIField(DisplayName = "Description")]
        public virtual string Description { getset; }
        #endregion
        #region CreatedByID
        public abstract class createdByID : IBqlField
        {
        }
        protected Guid? _CreatedByID;
        [PXDBCreatedByID()]
        public virtual Guid? CreatedByID
        {
            get
            {
                return this._CreatedByID;
            }
            set
            {
                this._CreatedByID = value;
            }
        }
        #endregion
        #region CreatedByScreenID
        public abstract class createdByScreenID : PX.Data.IBqlField
        {
        }
        protected String _CreatedByScreenID;
        [PXDBCreatedByScreenID()]
        public virtual String CreatedByScreenID
        {
            get
            {
                return this._CreatedByScreenID;
            }
            set
            {
                this._CreatedByScreenID = value;
            }
        }
        #endregion
        #region CreatedDateTime
        public abstract class createdDateTime : IBqlField
        {
        }
        protected DateTime? _CreatedDateTime;
        [PXDBCreatedDateTime()]
        public virtual DateTime? CreatedDateTime
        {
            get
            {
                return this._CreatedDateTime;
            }
            set
            {
                this._CreatedDateTime = value;
            }
        }
        #endregion
        #region LastModifiedByID
        public abstract class lastModifiedByID : IBqlField
        {
        }
        protected Guid? _LastModifiedByID;
        [PXDBLastModifiedByID()]
        public virtual Guid? LastModifiedByID
        {
            get
            {
                return this._LastModifiedByID;
            }
            set
            {
                this._LastModifiedByID = value;
            }
        }
        #endregion
        #region LastModifiedByScreenID
        public abstract class lastModifiedByScreenID : PX.Data.IBqlField
        {
        }
        protected String _LastModifiedByScreenID;
        [PXDBLastModifiedByScreenID()]
        public virtual String LastModifiedByScreenID
        {
            get
            {
                return this._LastModifiedByScreenID;
            }
            set
            {
                this._LastModifiedByScreenID = value;
            }
        }
        #endregion
        #region LastModifiedDateTime
        public abstract class lastModifiedDateTime : IBqlField
        {
        }
        protected DateTime? _LastModifiedDateTime;
        [PXDBLastModifiedDateTime()]
        public virtual DateTime? LastModifiedDateTime
        {
            get
            {
                return this._LastModifiedDateTime;
            }
            set
            {
                this._LastModifiedDateTime = value;
            }
        }
        #endregion
        #region tstamp
        public abstract class Tstamp : PX.Data.IBqlField
        {
        }
        protected Byte[] _tstamp;
        [PXDBTimestamp]
        public virtual Byte[] tstamp
        {
            get
            {
                return this._tstamp;
            }
            set
            {
                this._tstamp = value;
            }
        }
        #endregion
    }
}

Limit Accounts

When I was asked to limit Accounts selector in Sales orders, I thought it would be easy task, so I went to definition of SOLine, and found the following C# code:

    [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
    [Account(typeof (SOLine.branchID), Visible = false)]
    public virtual int? SalesAcctID
    {
      get
      {
        return this._SalesAcctID;
      }
      set
      {
        this._SalesAcctID = value;
      }
    }

Every time I see custom attributes of Acumatica during development I have some thought in background like ups, surprises will come for sure.

And they come. But let's try to make substitution for SalesAcctID with help of ordinary PXSelector in SOORderEntryExt:

public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
    {
        [PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
        [PXDBInt]
        [PXSelector(typeof(Search2<Account.accountID, 
            InnerJoin<CompanyAccountOn<Account.accountIDEqual<CompanyAccount.accountID>>>, 
                Where<CompanyAccount.cmpBranchIDEqual<Current<SOLine.branchID>>>>),
            typeof(Account.accountID), typeof(Account.accountCD), typeof(Account.description))]
        [PXUIField(DisplayName = "Account CD")]
        protected void SOLine_SalesAcctID_CacheAttached(PXCache sender)
        {
        }
    }

nothing fancy. Take a look at final result:

as you can see from screen recording, filtering works wonderful, but after selection of Account, field remains empty. How to deal with that? In that case on rescue comes PXDimensionSelector!

I will tell you mine own definition of PXDimensionSelector: Attribute, that allows you to modify any custom Acumatica selector. For example you can use PXDimensionSelector for filtering Accounts, SubAccounts ( but for that you'll need to play with  Segmented keys), etc.

Take a look on the code, which replaces PXSelector with PXDimensionSelector:

public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
    {
        [PXUIField(DisplayName = "Account", FieldClass = "ACCOUNT", Visibility = PXUIVisibility.Visible)]
        [PXDBInt]
        [PXDimensionSelector(AccountAttribute.DimensionName,
            typeof(Search2<Account.accountIDInnerJoin<CompanyAccountOn<Account.accountIDEqual<CompanyAccount.accountID>>>,
                Where<CompanyAccount.cmpBranchIDEqual<Current<SOLine.branchID>>>>), typeof(Account.accountCD),
            new Type[] { typeof(Account.accountCD), typeof(CompanyAccount.description) })]
        protected void SOLine_SalesAcctID_CacheAttached(PXCache sender)
        {
        }
    }
And final result:

As you can see from the screen recording, PXDimensionSelector allows you to filter account as well as select, while PXSelector only filters accounts, but doesn't allow you to select proper account

Update 2020-02-14

PXDimensionSelector helps to extend and manage SegmentMask Acumatica selector. It means if you need to modify something that contains SegmentMask, then you are good to go and use PXDimensionSelector.

How to work with selector

Hello everybody,

today I want to write few words how to work with selectors in Acumatica.

So, imagine, you have the following selector in Acumatica:

<px:PXSelector ID="NmbUsrBillPMTask" runat="server" DataField="UsrBillPMTask"/>

In order to make it work as selector, you need to do the following:

[PXSelector(typeof(Search<PMTask.taskID>))]

if you'll do it, you'll receive following selector:

And if you choose there some value, you'll be able to see id represented as number. In my case it was 727.

But what if you want to make it more readable?

Then you can add following part:

[PXSelector(typeof(Search<PMTask.taskID>), SubstituteKey = typeof(PMTask.taskCD))]

Then instead of some strange number Acumatica will display TaskCD field. 

But what if you have desire to add as display one more field displayed after "-" sign, as it is in some Acumatica screens. You can add another element, which is named DescriptionField. 

And final part of advice, you can also customize which fields should go in the selector. Take see at the following code:

[PXSelector(typeof(Search<PMTask.taskID,
            Where<PMTask.projectID, Equal<Current<PMTask.projectID>>>>), new Type[]{typeof (PMTask.taskCD), typeof (PMTask.locationID),
                typeof (PMTask.description), typeof (PMTask.status)}, SubstituteKey = typeof(PMTask.taskCD), DescriptionField = typeof(PMTask.description))]

You can try to figure out details at screenshot:

Hope that can help somebody to create selector.

PXSmartPanel in Acumatica

Hello everybody,

today I want to write few words how to work with PXSmartPanel.

One of my clients asked following:

1. Press at button.

2. Pop up should appear with two buttons ( "OK" , "Cancel")

3. At pop up should be also selector of contracts and datetime.

4. Existing screen should be modified.

In order to do this, following actions were implemented:

1. Existing screen was CT301000.

2. If to look in screen source code, following line says about graph:

<px:PXDataSource ID="ds" runat="server" Visible="True" Width="100%" TypeName="PX.Objects.CT.ContractMaint"
        PrimaryView="Contracts" BorderStyle="NotSet">

3. It means, that we need to create extension class for ContractMaint.

4. For example like this:

public class ContractMaintExt : PXGraphExtension<ContractMaint>
{

        [PXCopyPasteHiddenView]
        public PXFilter<SubstituteSettingsFilter> SubstituteSettings;
        public PXAction<Contract> Substitute;
        [PXButton(CommitChanges = true)]
        [PXUIField(DisplayName = "Substitute")]
        protected void substitute()
        {
            if (this.SubstituteSettings.AskExt(new PX.Data.PXView.InitializePanel(this.fillFilter)) ==
                WebDialogResult.OK)
            {
                var subsFilter = (SubstituteSettingsFilter)this.SubstituteSettings.Cache.Current;
                CopyOpenPMTaskHistory(Base.Contracts.Current.ContractID, subsFilter.ContractID, subsFilter.TransitionDate);
            }
        }
}

5. In the line 4 we declared to Acumatica, that we want to extend ContractMaint, and we want to create some data input, which is related to SubstituteSettingsFilter. Take note of Substitute and substitute: both of them are needed in order to make processing of inputted values.

6. Take a look at SubstituteSettingsFilter class itself:

 [Serializable]
        public class SubstituteSettingsFilter  : IBqlTable
        {
            #region contractID

            public abstract class contractID : IBqlField, IBqlOperand
            {
            }

            protected int? _ContractID;
            [PXDefault]
            [PXDBInt]
            [PXDimensionSelector("CONTRACT", typeof(Search2<Contract.contractID, 
                InnerJoin<ContractBillingSchedule, On<Contract.contractID, Equal<ContractBillingSchedule.contractID>>, 
                LeftJoin<Customer, On<Customer.bAccountID, Equal<Contract.customerID>>>>, 
                Where<Contract.isTemplate, Equal<boolFalse>,
                And<Contract.baseType, Equal<Contract.ContractBaseType>>>>), 
                typeof(Contract.contractCD), 
                new System.Type[] { typeof(Contract.contractCD), typeof(Contract.customerID), typeof(Customer.acctName), 
                    typeof(Contract.locationID), typeof(Contract.description), typeof(Contract.status), typeof(Contract.expireDate), 
                    typeof(ContractBillingSchedule.lastDate), typeof(ContractBillingSchedule.nextDate) },
                    DescriptionField = typeof(Contract.description), Filterable = true)]
            [PXUIField(DisplayName = "Contract ID", Visibility = PXUIVisibility.SelectorVisible)]
            [PXFieldDescription]
            
            public virtual int? ContractID
            {
                get { return this._ContractID; }
                set { this._ContractID = value; }

            }

            #endregion contractID

            #region TransitionDate
            public abstract class transitionDate : PX.Data.IBqlField
            {
            }
            protected DateTime? _TransitionDate;
            [PXDBDate()]
            [PXDefault]
            [PXUIField(DisplayName = "Transition Date")]
            public virtual DateTime? TransitionDate
            {
                get
                {
                    return this._TransitionDate;
                }
                set
                {
                    this._TransitionDate = value;
                }
            }
            #endregion TransitionDate
         }

7. We described two properties: contractID and TransitionDate.

8. Now goes time of aspx page at <px:PXFormView> section I described the following:

<px:PXSmartPanel ID="pnlSubstitute" runat="server" AcceptButtonID="PXButtonOK" AutoReload="true" CancelButtonID="PXButtonCancel"
        Caption="Substitute" CaptionVisible="True" DesignView="Content" HideAfterAction="false" Key="SubstituteSettings"
        LoadOnDemand="true" Height="190px" Width="360px" DefaultControlID="edContract">
        <px:PXFormView ID="PXFormView6" runat="server" CaptionVisible="False" DataMember="SubstituteSettings" Width="100%"
            DefaultControlID="edActivationDate" DataSourceID="ds" TabIndex="1400">
            <ContentStyle BackColor="Transparent" BorderStyle="None">
            </ContentStyle>
            <Template>
                <px:PXLayoutRule runat="server" StartColumn="True" LabelsWidth="SM" ControlSize="M" />
                <px:PXSelector runat="server" ID="edContract" DataField="ContractID" CommitChanges="True" Width="130px" />
                <px:PXDateTimeEdit CommitChanges="True" ID="edTransitionDate" runat="server" DataField="TransitionDate" Width="130px" />
                <px:PXLayoutRule runat="server" StartRow="True" />
                <px:PXPanel ID="PXPanel1" runat="server" SkinID="Buttons">
                    <px:PXButton ID="OK" runat="server" DialogResult="OK" Text="OK" />
                    <px:PXButton ID="Cancel" runat="server" DialogResult="Cancel" Text="Cancel" />
                </px:PXPanel>
            </Template>
        </px:PXFormView>
</px:PXSmartPanel>

Few words. AcceptButtonID speak about who plays role of OK button, Caption means title, CaptionVisible means that title will be visible, HideAfterAction means hide or not after action, Key means name of member in extension or graph, DefaultControlID means which control is active by default. Another important detail is DataMember property, which equals to Key.  Then goes layout rules, which I ommit. Then in Template goes selector, and datetime input. 

9. Here how control look like:

Here it is. If you have any comments, wishes, etc, you are welcome to post them