How to use OrderBy in a data view delegate - acumatica

I have a custom processing screen that includes a drop down box, provided for the user. The drop down is used to provide the sort order that is consumed in a data view delegate. The reason I chose a data view delegate is the query is entirely dependent on the sort order chosen. Based on the selection, the query must order the results, along with additional logic, then post the resultset to the grid. I use a switch statement to evaluate the selection chosen in the drop down. Currently the resultset returns the results of the query. But the sort order is based on the declared data view. My order by clause, in the switch statement, is not consumed. I believe the sort order is based on the keys in the defined data view, by default. How can I dynamically re-order the result of the query?
switch (filter.ProcessOrder)
{
//new[] { "O", "P", "R", "B" },
//new[] { "By Order Date", "By Order Priority", "By Request Date", "By Backorder Only" }
case "O":
foreach (PXResult<SOOrder, SOOrderShipment> res in PXSelectJoin<SOOrder,LeftJoin<SOOrderShipment, On<SOOrder.orderNbr, Equal<SOOrderShipment.orderNbr>, And<SOOrder.orderType, Equal<SOOrderShipment.orderType>>>>,
Where<SOOrderShipment.shipmentNbr, IsNull, And<SOOrder.orderType, Equal<OrderTypes.salesOrder>>>,
OrderBy<Desc<SOOrder.orderDate>>>.Select(this))
{
SOOrder order = (SOOrder)res;
SOOrderShipment oShip = (SOOrderShipment)res;
//ASSIGN VALUES OR EXCLUDE FRON RESULTSET BASED ON CRITERIA TO BE DEFINED
yield return new PXResult<SOOrder, SOOrderShipment>(order, oShip);
}
break;

The results list returned by the view delegate is always sorted according to the conditions specified in the view's select. As you have guessed correctly, if you don't specify any sorting conditions in the select, the results will be sorted by keys.
There is however a default approach to overcome this. You may alter the desired sorting conditions for the view using the OrderByNew method of PXSelectBase or PXView in your delegate like this:
public PXSelect<SOOrder> Orders;
public virtual IEnumerable orders()
{
switch(filter.ProcessOrder)
{
case "O": // by Order Date
foreach (PXResult<SOOrder, SOOrderShipment> res in PXSelectJoin<SOOrder,LeftJoin<SOOrderShipment, On<SOOrder.orderNbr, Equal<SOOrderShipment.orderNbr>, And<SOOrder.orderType, Equal<SOOrderShipment.orderType>>>>,
Where<SOOrderShipment.shipmentNbr, IsNull, And<SOOrder.orderType, Equal<OrderTypes.salesOrder>>>,
OrderBy<Desc<SOOrder.orderDate>>>.Select(this))
{
SOOrder order = (SOOrder)res;
SOOrderShipment oShip = (SOOrderShipment)res;
//ASSIGN VALUES OR EXCLUDE FRON RESULTSET BASED ON CRITERIA TO BE DEFINED
yield return new PXResult<SOOrder, SOOrderShipment>(order, oShip);
}
// !!!
Orders.OrderByNew<SOOrder.OrderDate>();
// !!!
break;
// Other cases
}
}
Using the clause like above but with different fields in different cases, you may achieve the desired result.

I had a similar case,
What i did was, added a new unbound int column named SortOrder, and i manually sort the result and return it from delegate.
#region SortOrder
public abstract class sortOrder : PX.Data.IBqlField
{
}
protected Int32? _SortOrder;
[PXDefault(TypeCode.Int32, "5")]
[PXInt()]
public virtual Int32? SortOrder
{
get
{
return this._SortOrder;
}
set
{
this._SortOrder = value;
}
}
#endregion
Declare the view with sorting based on the new column
public PXSelect<Something,
Where<True, Equal<False>>, OrderBy<Asc<Something.sortOrder>>> SomethingElse; // My delegate will give the result always
In the delegate, get the result and set the sortorder value of your resultset records and return the result.
and it was working ^^

Related

How to use a Multi-Select String field in a BQL Where

I have a field in the unbound filter DAC for a custom processing screen that is a multi-select dropdown field. It has the PXStringList attribute defined like this:
#region POStatus
[PXString]
[PXDefault(PersistingCheck = PXPersistingCheck.Nothing)]
[PXUIField(DisplayName = "PO Status")]
[PXStringList(new string[] { "N", "B" }, new string[] { "Open", "Pending Approval" }, MultiSelect =true)]
public string POStatus { get; set; }
public abstract class poStatus : PX.Data.BQL.BqlString.Field<poStatus> { }
#endregion
How can I use this field in the Where clause of the BQL query for the main view? So, in this case I'm trying to filter POOrder records by whatever statuses the user selects in the multi-select drop down.
I tried using the IsIn<> operator but, it doesn't build.
Do I have to resort to a view delegate in order to do this?
TIA!
Acumatica stores the values for the multi-select drop-downs in 1 field as comma-separated values. The first thing I would try would be to try using the Contains operation in opposite direction like below:
PXSelect<POOrder, Where<Current<FilterDAC.poStatus>, Contains<POOrder.status>>> FilteredPOList;

How to Override the InventoryCD Number Sequence

I have a request to change the number sequencer for a new Item based on the Class that is selected for the new item. So, the user selects a particular class and the system need to find the next value for the InventoryItem.InventoryCD by using a number sequence that is set on a field in a preferences form.
Any ideas on how to do this?
I've tried the FieldDefaulting() event for the InventoryCD and InventoryID fields but, it's not working.
TIA!
A custom numbering attribute is required to achieve the functionality you desire. Please find working example below.
First we define the fields on setup which will hold our custom numbering sequences :
public class INSetupExtension : PXCacheExtension<INSetup>
{
#region InventoryNumbering1ID
public abstract class inventoryNumbering1ID : PX.Data.BQL.BqlString.Field<inventoryNumbering1ID>
{
}
[PXDBString(10, IsUnicode = true)]
[PXDefault("INV1")]
[PXSelector(typeof(Numbering.numberingID), DescriptionField = typeof(Numbering.descr))]
[PXUIField(DisplayName = "Inventory Numbering One", Visibility = PXUIVisibility.Visible)]
public virtual String InventoryNumbering1ID { get; set; }
#endregion
#region InventoryNumbering1ID
public abstract class inventoryNumbering2ID : PX.Data.BQL.BqlString.Field<inventoryNumbering2ID>
{
}
[PXDBString(10, IsUnicode = true)]
[PXDefault("INV2")]
[PXSelector(typeof(Numbering.numberingID), DescriptionField = typeof(Numbering.descr))]
[PXUIField(DisplayName = "Inventory Numbering Two", Visibility = PXUIVisibility.Visible)]
public virtual String InventoryNumbering2ID { get; set; }
#endregion
}
Next we define our custom Autonumbering attribute, here I have made the "default" numbering sequence the argument for the constructor declaration.
In the overridden RowPersisting event we utilize our criteria to determine the numbering sequence to use for the next persisted new item.
In my example I use one sequence if an item class is used, if no item class is selected I use another.
public class NonStockAutoNumbereAttribute : AutoNumberAttribute
{
public NonStockAutoNumbereAttribute() : base(typeof(INSetupExtension.inventoryNumbering2ID), typeof(AccessInfo.businessDate))
{
}
public override void RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
{
InventoryItem row = e.Row as InventoryItem;
if(sender.GetStatus(row) == PXEntryStatus.Inserted)
{
base.RowPersisting(sender, e);
INSetup setup = PXSelectBase<INSetup, PXSelectReadonly<INSetup>.Config>.Select(sender.Graph, Array.Empty<object>());
INSetupExtension setupExt = setup.GetExtension<INSetupExtension>();
string nextNumber;
object newValue = sender.GetValue(e.Row, this._FieldOrdinal);
if(!string.IsNullOrEmpty((string)newValue))
{
INItemClass itemClass = PXSelectReadonly<INItemClass, Where<INItemClass.itemClassID, Equal<Required<INItemClass.itemClassID>>>>.Select(sender.Graph, row.ItemClassID);
if(itemClass != null)
{
nextNumber = AutoNumberAttribute.GetNextNumber(sender, e.Row, setupExt.InventoryNumbering1ID, DateTime.Now);
}
else
{
nextNumber = AutoNumberAttribute.GetNextNumber(sender, e.Row, setupExt.InventoryNumbering2ID, DateTime.Now);
}
sender.SetValue(e.Row, this._FieldName, nextNumber);
}
}
}
}
On the Inventory Item maintenance page I have added a simple CacheAttached :
public class NonStockItemMaintExtension : PXGraphExtension<NonStockItemMaint>
{
[PXDefault]
[NonStockAutoNumbereAttribute]
[InventoryRaw(typeof(Where<InventoryItem.stkItem.IsEqual<False>>), IsKey = true, DisplayName = "Inventory ID", Filterable = true)]
protected virtual void _(Events.CacheAttached<InventoryItem.inventoryCD> e) { }
}
The results can be seen below :
No class numbering
Class Numbering
You mentioned trying to do this on FieldDefaulting and also that you attempted on InventoryID and InventoryCD. Before we get into a couple of possible options, let's focus on the nature of InventoryID vs. InventoryCD and the FieldDefaulting event. Also, we need to consider the impact of AutoNunmber on InventoryCD.
InventoryID vs. InventoryCD
InventoryID is an Identity field. It is a sequence generated by the database on insert to allow us to uniquely identify the record even if we change other common values such as the InventoryCD, as you desire. The InventoryCD field is also known as the Inventory Code. This is the human readable number that adheres to our desired nomenclature for defining a part number. For instance, we use a 2 digit year to prefix a number sequence to clearly indicate how old the part number is. Because InventoryID is an identity field, NEVER try to update that field, or you may cause data integrity issues for all the related tables that point back to the InventoryItem record via the InventoryID field.
FieldDefaulting and SetDefaultExt
FieldDefaulting may be the place you want to set the value for InventoryCD. However, you don't know the value at the time you are creating the record. In fact, you don't even know the value when you are ready to press the save button. Unless you have configured numbering to allow manual entry, the number isn't assigned until the Persist. As the value is inserted into the database, the AutoNumber logic fires to identify the numbering sequence to generate the InventoryCD value. If you wish to use the FieldDefaulting event, you must first enable manual numbering for the Inventory numbering sequence. You then want to be sure to use SetDefaultExt on the FieldUpdated event of the item class field so that when you change item classes, you get a new inventory id. You also must decide if this value is allowed to change once assigned because the aforementioned logic will change the value every time you change the item class unless you put other measures in place.
Change ID Action
It is important to note that there is a Change ID action on the Action menu. It is intended for this very purpose, although it is a manual entry. I'd recommend that you explore the code behind that action to understand how Acumatica goes about altering the InventoryCD. Also, note that this is performed manually after the record has been created and and InventoryCD has been assigned already. Perhaps you have a way to identify that the InventoryCD value is still the default and needs to be changed, so you could do something around overriding Persist.
Custom Attribute Inheriting AutoNumber
If you are more adventurous than I am, you could consider a custom Attribute inheriting from the AutoNumber attribute and use a cacheattached to change to your attribute.
Business Events to Initiate Custom Action
Finally, you might consider a business event to watch for when an InventoryItem record has been inserted and then fire some custom action that simulates the Change ID action but using your logic for determining the new number based on the Item Class. If you only capture when the record is created and not when it is updated, then I suspect you can use the business event to make the initial InventoryCD change per Item Class and then require manual change through Change ID if needed again later.
Each of these points gives you some possible ways to approach making the change you need as well as some potential hazards. While this doesn't give you the code achieve your objective, I hope that it gives you some options to find the solution that works best for you. Please post a comment or answer once you reach a solution that works best for you.

Table joins with PXDatabase SelectMulti

Disclaimer: I'm new to C# and Acumatica Framework
I'm looking to implement a database slot however I need to pull data from joined tables. I'm using the PXDatabase.SelectMulti method from the snippet below however, I have been unable to get it to work with joins. I also can't seem to find any examples of the method with joined tables.
Is there a way to join tables with this method or perhaps another way to query the data?
public class DatabaseSlotsExample : IPrefetchable
{
protected List<string> values = new List<string>(); // store your values here
public static List<string> Values
{
get
{
//Get the values from the slot dynamically. By providing table name, you inform system when it should reset the slot.
return PXDatabase.GetSlot<DatabaseSlotsExample>("SlotSuperKey", typeof(YourTable)).values;
}
}
public void Prefetch()
{
//read database here
foreach(PXDataRecord rec in PXDatabase.SelectMulti<YourTable>(
new PXDataField<YourTable.tableField>(), //definition for fields that system should select
new PXDataFieldValue<YourTable.tableKey>("Some Condition") //definition for restriction that you need to apply
))
{
//populate your collection from the database here
values.Add(rec.GetString(0));
}
}
}
Yes, if you use BQL.Fluent as a reference, the BQL query is also simplified. See below used on Service Orders:
foreach (PXResult<FSServiceOrder> res in
SelectFrom<FSServiceOrder>.
InnerJoin<FSAppointment>.On<FSAppointment.soRefNbr.IsEqual<FSServiceOrder.refNbr>>.
InnerJoin<FSWFStage>.On<FSWFStage.wFStageID.IsEqual<FSAppointment.wFStageID>>.
InnerJoin<FSRoom>.On<FSRoom.roomID.IsEqual<FSServiceOrder.roomID>>.
InnerJoin<FSEquipment>.On<FSEquipment.registrationNbr.IsEqual<FSRoom.descr>>.
Where<FSServiceOrder.srvOrdType.IsEqual<P.AsString>.
And<FSWFStage.wFStageCD.IsEqual<P.AsString>>>.
View.Select(this, "TO", "SCHEDULED")
{
FSServiceOrder fsServiceOrder = res.GetItem<FSServiceOrder>();
FSAppointment fsAppointment = res.GetItem<FSAppointment>();
}
Then you can use the below to pull the specific table data:

Joining table using PXSelectJoin using cast in BQL statement

I have a requirement to write a BQL statement for below SQL query
SELECT * FROM CSATTRIBUTEGROUP INNER JOIN INVENTORYITEM ON CSATTRIBUTEGROUP.ENTITYCLASSID=CAST(INVENTORYITEM.ITEMCLASSID AS NVARCHAR(10))
LEFT JOIN CSANSWERS ON INVENTORYITEM.NOTEID=CSANSWERS.REFNOTEID WHERE INVENTORYCD='CPU'
The EntityClassID in CSAttribute & ItemClassID in inventory are different type. How to join the table using BQL.
One would be tempted to create a custom view representing the INVENTORYITEM table with a cast of ITEMCLASSID. However custom views are not recommended. Instead try creating a PXProjection to represent the INVENTORYITEM table as usrINVENTORYITEM. Then use a PXString to convert the Int? to a string. Once exposed you can work out your BQL as shown below.
#region InventoryItem - projection
[PXProjection(typeof(Select<InventoryItem>), Persistent = false)]
public partial class usrInventoryItem : InventoryItem
{
#region ItemClassIDStr
[PXString(10, IsUnicode = true)]
[PXUIField(DisplayName = "ItemClassIDStr", Visibility = PXUIVisibility.SelectorVisible)]
public virtual string ItemClassIDStr
{
get
{
return $"{ItemClassID}";
}
set
{
this.ItemClassIDStr = value;
}
}
public abstract class itemClassIDStr : IBqlField { }
#endregion
}
#endregion
then the BQL statement:
// basic example of your join.
public PXSelectJoin<CSAttributeGroup, LeftJoin<usrInventoryItem,
On<usrInventoryItem.itemClassIDStr, Equal<CSAttributeGroup.entityClassID>>>> ExampleJoin;

Check if ListBoxFor selectedValues is null before display in view?

I have a number of ListBoxFor elements on a form in edit mode. If there was data recorded in the field then the previously selected items are displaying correctly when the form opens. If the field is empty though an error is thrown as the items parameter cannot be null. Is there a way to check in the view and if there is data to use the ListBoxFor with the four parameters but if there isn't to only use three parameters, leaving out the selected items?
This is how I'm declaring the ListBoxFor:
#Html.ListBoxFor(model => model.IfQualityPoor, new MultiSelectList(ViewBag.IfPoor, "Value", "Text", ViewBag.IfQualityPoorSelected), new { #class = "chosen", multiple = "multiple" })
I'm using the ViewBag to pass the ICollection which holds the selected items as the controller then joins or splits the strings for binding to the model field. The MultiSelectLists always prove problematic for me.
Your question isn't entirely clear, but you're making it way harder on yourself than it needs to be using ListBoxFor. All you need for either DropDownListFor or ListBoxFor is an IEnumerable<SelectListItem>. Razor will take care of selecting any appropriate values based on the ModelState.
So, assuming ViewBag.IfPoor is IEnumerable<SelectListItem>, all you need in your view is:
#Html.ListBoxFor(m => m.IfQualityPoor, (IEnumerable<SelectListItem>)ViewBag.IfPoor, new { #class = "chosen" })
The correct options will be marked as selected based on the value of IfQualityPoor on your model, as they should be. Also, it's unnecessary to pass multiple = "multiple" in in your htmlAttributes param, as you get that just by using ListBoxFor rather than DropDownListFor.
It's even better if you use a view model and then add your options as a property. Then, you don't have to worry about casting in the view, which is always a good way to introduce runtime exceptions. For example:
public class FooViewModel
{
...
public IEnumerable<SelectListItem> IfQualityPoorOptions { get; set; }
}
Then, you set this in your action, before returning the view (instead of setting ViewBag). Finally, in your view:
#Html.ListBoxFor(m => m.IfQualityPoor, Model.IfQualityPoorOptions, new { #class = "chosen" })
Much simpler, and you'll never have any issues doing it that way.
UPDATE (based on comment)
The best way to handle flattening a list into a string for database storage is to use a special property for that, and then custom getter and setter to map to/from. For example:
public string IfQualityPoor
{
get { return IfQualityPoorList != null ? String.Join(",", IfQualityPoorList) : null; }
set { IfQualityPoorList = !String.IsNullOrWhiteSpace(value) ? value.Split(',').ToList() : null; }
}
[NotMapped]
public List<string> IfQualityPoorList { get; set; }
Then, you post to/interact with IfQualityPoorList, and the correct string will be set in the database automatically when you save.

Resources