I have a field which shows the attribute of an item called "Coating." I added this field via the Layout Editor to two existing screens I am customizing: The Sales Price and Sales Price Worksheets pages. On the Sales Price Worksheet page, the coatings show up just fine:
However, on the Sales Price page, they do not:
I have the exact same element in both page customization in the Layout Editor contained in the respected grids: InventoryItem__COATING_Attributes. Checking the Attributes tab of the element, they both have the same code:
protected string[] _Attributes;
/// <summary>
/// Reserved for internal use.
/// Provides the values of attributes associated with the item.
/// For more information see the <see cref="CSAnswers"/> class.
/// </summary>
[CRAttributesField(typeof(InventoryItem.itemClassID))]
As far as I can tell, everything is exactly the same. I even checked the results of the query that is sent for both pages' select statements and they are properly returning similar statements to show the coatings for each element.
Any ideas on why this isn't working?
I assume the attribute works as expected on the Sales Price Worksheets screen due to the sample posted in another thread and the lack of delegate declared for the ARPriceWorksheetMaint.Details data view:
public class ARPriceWorksheetMaint : PXGraph<ARPriceWorksheetMaint, ARPriceWorksheet>
{
...
[PXImport(typeof(ARPriceWorksheet))]
public PXSelectJoin<ARPriceWorksheetDetail,
LeftJoin<InventoryItem, On<InventoryItem.inventoryID, Equal<ARPriceWorksheetDetail.inventoryID>>>, Where<ARPriceWorksheetDetail.refNbr, Equal<Current<ARPriceWorksheet.refNbr>>>,
OrderBy<Asc<ARPriceWorksheetDetail.priceType, Asc<ARPriceWorksheetDetail.priceCode, Asc<InventoryItem.inventoryCD, Asc<ARPriceWorksheetDetail.breakQty>>>>>> Details;
...
}
On the Sales Price screen though, there is a records() delegate returning only instances of the ARSalesPrice DAC, preventing the attribute from obtaining its values from database:
public virtual IEnumerable records()
{
...
foreach (PXResult<ARSalesPrice> res in QSelect(this, Records.View.BqlSelect, new object[] { filter.PriceType, filter.PriceType, filter.PriceType == PriceTypes.Customer ? priceCode : null, filter.PriceType == PriceTypes.CustomerPriceClass ? priceCode : null, priceCode, filter.InventoryID, filter.InventoryID, filter.EffectiveAsOfDate, filter.EffectiveAsOfDate, filter.EffectiveAsOfDate, filter.ItemClassID, filter.ItemClassID, filter.InventoryPriceClassID, filter.InventoryPriceClassID, filter.OwnerID, filter.OwnerID, filter.MyWorkGroup, filter.WorkGroupID, filter.WorkGroupID }))
{
ARSalesPrice price = res;
yield return price;
}
...
}
In order to show the attribute on the Sales Price screen, you should modify the Records data view and override its delegate to return instances of both the ARSalesPrice and InventoryItem DACs:
public class ARSalesPriceMaintExt : PXGraphExtension<ARSalesPriceMaint>
{
[PXFilterable]
public PXSelectJoin<ARSalesPrice,
LeftJoin<InventoryItem, On<InventoryItem.inventoryID, Equal<ARSalesPrice.inventoryID>>>,
Where<InventoryItem.itemStatus, NotEqual<INItemStatus.inactive>,
And<InventoryItem.itemStatus, NotEqual<INItemStatus.toDelete>,
And2<Where<Required<ARSalesPriceFilter.priceType>, Equal<PriceTypes.allPrices>, Or<ARSalesPrice.priceType, Equal<Required<ARSalesPriceFilter.priceType>>>>,
And2<Where<ARSalesPrice.customerID, Equal<Required<ARSalesPriceFilter.priceCode>>, Or<ARSalesPrice.custPriceClassID, Equal<Required<ARSalesPriceFilter.priceCode>>, Or<Required<ARSalesPriceFilter.priceCode>, IsNull>>>,
And2<Where<ARSalesPrice.inventoryID, Equal<Required<ARSalesPriceFilter.inventoryID>>, Or<Required<ARSalesPriceFilter.inventoryID>, IsNull>>,
And2<Where2<Where2<Where<ARSalesPrice.effectiveDate, LessEqual<Required<ARSalesPriceFilter.effectiveAsOfDate>>, Or<ARSalesPrice.effectiveDate, IsNull>>,
And<Where<ARSalesPrice.expirationDate, GreaterEqual<Required<ARSalesPriceFilter.effectiveAsOfDate>>, Or<ARSalesPrice.expirationDate, IsNull>>>>,
Or<Required<ARSalesPriceFilter.effectiveAsOfDate>, IsNull>>,
And<Where2<Where<Required<ARSalesPriceFilter.itemClassID>, IsNull,
Or<Required<ARSalesPriceFilter.itemClassID>, Equal<InventoryItem.itemClassID>>>,
And2<Where<Required<ARSalesPriceFilter.inventoryPriceClassID>, IsNull,
Or<Required<ARSalesPriceFilter.inventoryPriceClassID>, Equal<InventoryItem.priceClassID>>>,
And2<Where<Required<ARSalesPriceFilter.ownerID>, IsNull,
Or<Required<ARSalesPriceFilter.ownerID>, Equal<InventoryItem.priceManagerID>>>,
And2<Where<Required<ARSalesPriceFilter.myWorkGroup>, Equal<False>,
Or<InventoryItem.priceWorkgroupID, InMember<CurrentValue<ARSalesPriceFilter.currentOwnerID>>>>,
And<Where<Required<ARSalesPriceFilter.workGroupID>, IsNull,
Or<Required<ARSalesPriceFilter.workGroupID>, Equal<InventoryItem.priceWorkgroupID>>>>>>>>>>>>>>>,
OrderBy<Asc<ARSalesPrice.inventoryID,
Asc<ARSalesPrice.priceType,
Asc<ARSalesPrice.uOM, Asc<ARSalesPrice.breakQty, Asc<ARSalesPrice.effectiveDate>>>>>>> Records;
public IEnumerable records()
{
var startRow = PXView.StartRow;
int totalRows = 0;
foreach (ARSalesPrice salesPrice in Base.Records.View.Select(PXView.Currents, PXView.Parameters, PXView.Searches,
PXView.SortColumns, PXView.Descendings, PXView.Filters, ref startRow, PXView.MaximumRows, ref totalRows))
{
var item = PXSelectorAttribute.Select<ARSalesPrice.inventoryID>(Records.Cache, salesPrice) as InventoryItem;
var res = new PXResult<ARSalesPrice, InventoryItem>(salesPrice, item);
yield return res;
}
PXView.StartRow = 0;
}
}
Related
Is it possible to use a foreach loop in a BLC to iterate through the fields of a PXResultSet to get the FieldNames?
Is this doable? I can't seem to find a good way.
Thanks...
The PXResultset records are selected from a view. You can get the field names from the View.
Here's a full example:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public override void Initialize()
{
// Get field list from data view
var dataView = new PXSelect<SOOrder>(Base);
string fieldNames = string.Join(",", GetFieldNames(dataView.View, Base.Caches));
// You don't need result set to get field names
PXResultset<SOOrder> resultSet = dataView.Select();
throw new PXException(fieldNames);
}
public string[] GetFieldNames(PXView view, PXCacheCollection caches)
{
var list = new List<string>();
var set = new HashSet<string>();
foreach (Type t in view.GetItemTypes())
{
if (list.Count == 0)
{
list.AddRange(caches[t].Fields);
set.AddRange(list);
}
else
{
foreach (string field in caches[t].Fields)
{
string s = String.Format("{0}__{1}", t.Name, field);
if (set.Add(s))
{
list.Add(s);
}
}
}
}
return list.ToArray();
}
}
When run, this example will show the fields names used in the data view in Sales Order screen SO301000 as an exception.
Field names are contained in Cache object. If you really need to get field names from PXResultset you need to iterate the cache types in the result set.
Example for first DacType (0) of result set:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public override void Initialize()
{
var dataView = new PXSelect<SOOrder>(Base);
PXResultset<SOOrder> resultSet = dataView.Select();
foreach (PXResult result in resultSet)
{
Type dacType = result.GetItemType(0);
foreach (var field in Base.Caches[dacType].Fields)
PXTrace.WriteInformation(field);
}
}
}
I have 50+ custom paired fields "Inches" and "Centimeters", each enabled and editable. I need to update "Inches" if the user changed the value of "Centimeters" and visa verse. I was able to do this using SetValuePending on one of the paired fields and SetValueExt on the other during the Field Updated Event. My question, is there a way to do this on a higher level without having to do a Field_Updated event for all the 100+ fields. I know that Formulas would create a circular reference so cannot be used. Thanks
Well, you can use one method to handle FieldUpdated events for all fields you need using graph FieldUpdated.AddHandler method in constructor. To get a field name just extend a standard Acumatica FieldUpdated delegate with one additional parameter (name for example) and put it during the FieldUpdated.AddHandler call.
Here is an example with "Invoices and Memos" screen and ARInvoiceEntry graph.
public ARInvoiceEntry()
{
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.inches).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.inches).Name));
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.centimeters).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.centimeters).Name));
...
}
protected virtual void CommonFieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e, string name)
{
// do something
}
Moreover, you can add handlers dynamically using fields collection for example
foreach(var field in Caches[typeof(ARTran)].Fields)
{
// add handler depends on field name
}
You can do it by PXFormula + ExternalValue BQL expression (the same as PXRowUpdatedEventArgs.ExternalCall for example), which will prevent circular reference between pair fields. The idea is to calculate field only when a related field has been changed by the user from UI (ExternalCall = true) and skip calculation when related field updated by the formula (ExternalCall = false).
public class centimetersInInches : PX.Data.BQL.BqlDecimal.Constant<centimetersInInches>
{
public centimetersInInches() : base(2.54m) { }
}
[PXDecimal]
[PXUIField(DisplayName = "Inches")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Div<centimeters, centimetersInInches>>))]
public virtual decimal? Inches { get; set; }
public abstract class inches : PX.Data.BQL.BqlDecimal.Field<inches> { }
[PXDecimal]
[PXUIField(DisplayName = "Centimeters")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Mult<inches, centimetersInInches>>))]
public virtual decimal? Centimeters { get; set; }
public abstract class centimeters : PX.Data.BQL.BqlDecimal.Field<centimeters> { }
And aspx
<px:PXGridColumn DataField="Inches" CommitChanges="True" />
<px:PXGridColumn DataField="Centimeters" CommitChanges="True" />
You can use the RowUpdated event and compare the old row with the new row to detect which field changed. I agree that keeping all logic in a single method is preferable.
public virtual void DAC_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
DAC row = e.Row as DAC;
DAC oldRow = e.OldRow as DAC;
if (row == null || oldRow == null) return;
// Compare old row with new row to determine which field changed
if (row.Inches != oldRow.Inches)
{
// Inches field changed, update CM value
row.CM = row.Inches * INCHES_TO_CM_CONSTANT;
}
// Add more conditions for the other fields
[..]
}
I know that Formulas would create a circular reference so cannot be
used.
Yes I wouldn't recommend it either, you could use the DAC property Setter though.
public string _Inches
public virtual string Inches
{
get
{
return this._Inches;
}
set
{
this._Inches = value;
this.CM = value * INCHES_TO_CM_CONSTANT;
}
}
For all solution (except Formula/DAC attributes) I think a condition to stop recursion should be possible if it's absolutely necessary:
if (this.CM != value * INCHES_TO_CM_CONSTANT)
this.CM = value * INCHES_TO_CM_CONSTANT;
Ideally proper use/avoidance of SetValueExt to control when events are raised (Ext method raises events) would be enough to stop infinite loops.
am generating a pop on one new button click in stock Items screen, inside popup have two buttons, user select some data from fields in the popup and then click on ok, then inventoryId should be formed with some special characters and then it should display in InventoryID field. I can form the InventoryID but unable to display it in InventoryID field.
[PXButton]
[PXUIField(DisplayName = "Generate")]
public virtual void GenerateInv()
{
InventoryItemExt ext = Base.Item.Cache.GetExtension<InventoryItemExt>(Base.Item.Current);
Base.Item.Current.InventoryCD = ext.UsrInvId;
Base.Item.Cache.Insert(Base.Item.Current);
}
I had this issue before,
and the following code was provided by acumatica. you may try the same.
here in the below code, inventoryCD contains the value, i wanted to set.
InventoryItem item = PXCache<InventoryItem>.CreateCopy(Base.Item.Current);
OrderedDictionary keys = new OrderedDictionary(StringComparer.OrdinalIgnoreCase)
{
{
typeof(InventoryItem.inventoryCD).Name,
adapter.View.Cache.GetValue(adapter.View.Cache.Current,
typeof(InventoryItem.inventoryCD).Name)
}
};
OrderedDictionary vals = new OrderedDictionary(StringComparer.OrdinalIgnoreCase)
{
{
typeof(InventoryItem.inventoryCD).Name, inventoryCD
}
};
adapter.View.Cache.Update(keys, vals);
if (adapter.Searches != null && adapter.Searches.Length > 0)
{
adapter.Searches[0] = inventoryCD;
}
return adapter.Get();
Also make sure your function have the correct input and the return type
IEnumerable GenerateInv(PXAdapter adapter)
I need to obtain a dataset or array of the results provided by a Generic Inquiry inside of BLC / graph logic.
I've been given the following as an example, but this obviously only returns a count.
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> Test;
[PXButton]
[PXUIField]
protected void test()
{
var dataGraph = PXGenericInqGrph.CreateInstance("GI000010");
if (dataGraph != null)
{
var count = dataGraph.Views["Results"].SelectMulti().Count;
}
}
}
When I use an index in the returned variable, I don't get anything that resembles a row of the actual data in the GI, for example:
dataGraph.Views["Results"].SelectMulti()[0]
This doesn't return the actual data. I've tried the several methods / properties provided by the dataGraph.Views["Results"] object, but none give me the results I need.
Is there a method or property that simply returns a dataset or array of the actual results of the Generic Inquiry?
The following custom button I developed for Non-Stock Items will call the specified Generic Inquiry, update its parameter value and retrieve all result rows and corresponding field results without needing to know the specific DAC.
public class NonStockItemMaintExtension : PXGraphExtension<NonStockItemMaint>
{
#region Buttons
public PXAction<InventoryItem> RunSummary;
[PXButton]
[PXUIField(DisplayName = "Run Summary")]
public virtual IEnumerable runSummary(PXAdapter adapter)
{
InventoryItem item = Base.Item.Current;
GIDesign design = PXSelectReadonly<GIDesign, Where<GIDesign.name, Equal<Required<GIDesign.name>>>>.Select(this.Base, "ItemTrans");
if (design != null)
{
PXLongOperation.StartOperation(this.Base, delegate ()
{
//Creates Generic Inquiry Graph for the specified inquiry
PXGenericInqGrph graph = PXGenericInqGrph.CreateInstance(design.DesignID.Value);
if (design != null)
{
//Sets parameter value
graph.Caches[typeof(GenericFilter)].SetValueExt(graph.Filter.Current, "Item", item.InventoryCD);
//Loops through each returned result row of Generic Inquiry
foreach (GenericResult resultRow in graph.Views["Results"].SelectMulti())
{
//Loops through objects returned from one - not an object per field
foreach (string key in resultRow.Values.Keys)
{
//Loops through list of each object and the fields we need values from for each data key
foreach (GIResult resultMap in PXSelectReadonly<GIResult, Where<GIResult.designID, Equal<Required<GIResult.designID>>, And<GIResult.objectName, Equal<Required<GIResult.objectName>>>>>.Select(graph, new object[] { design.DesignID.Value, key }))
{
//retrieves field value from data object specified
var result = graph.Caches[resultRow.Values[key].GetType()].GetValue(resultRow.Values[key], resultMap.Field);
}
}
}
}
});
}
return adapter.Get();
}
#endregion
}
Tables referenced in Generic Inquiry :
Data fields to retrieve from the inquiry :
Parameters being populated :
Value being retrieved :
This will give you a list of the results. Each list element will contain the records involved with 1 row of the GI result (each joined table). Only those fields included in the GI will have values I believe.
public PXAction<SOOrder> Test;
[PXButton]
[PXUIField]
protected void test()
{
// Using "Invoiced Items" (GI000008) GI in 2017R2
var dataGraph = PXGenericInqGrph.CreateInstance("GI000008");
if (dataGraph == null)
{
return;
}
var resultList = dataGraph.Results.Select().FirstTableItems.ToList();
foreach (GenericResult genericResult in resultList)
{
// Note: not all values are pulled into the DAC, only those used in the query
var arInvoice = GetDac<PX.Objects.AR.ARInvoice>(genericResult);
var arTran = GetDac<PX.Objects.AR.ARTran>(genericResult);
var customer = GetDac<PX.Objects.AR.Customer>(genericResult);
var customerClass = GetDac<PX.Objects.AR.Customer>(genericResult);
var address = GetDac<PX.Objects.AR.Customer>(genericResult);
var bAccount = GetDac<PX.Objects.CR.BAccount>(genericResult);
var inventoryItem = GetDac<PX.Objects.IN.InventoryItem>(genericResult);
var formulaValues = genericResult.Values.Last();
}
}
protected virtual T GetDac<T>(GenericResult genericResult) where T : class, PX.Data.IBqlTable
{
// Example:
//var customer = (PX.Objects.AR.Customer)genericResult.Values["Customer"]
return (T)(genericResult?.Values != null &&
genericResult.Values.ContainsKey(typeof(T).Name)
? genericResult.Values[typeof(T).Name]
: null);
}
In Sales Order documents grid footer. It displays the product's availability.
How to do the same in Opportunity products grid ?
More so, how do you enforce it to be displayed at the footer
instead of a simple grid column ? Is there such attribute ?
Thanks for the replies.
If we compare to sales order, the sales order line gets its value from LSSOLine during Availabilty_FieldSelecting. The wire-up on the page is on the tab via StatusField="Availability". We can do something similar by adding an unbound extension field and then during a field selecting fill in the value. An alternative would be to implement an LSCROpportunityProducts class that is inherits LSSelect similar to LSSoLine (a better preferred solution). To keep this simple and focus on just getting the field to display text, I will use an extension field and a simple field selecting in the extension graph for opportunity.
(1) In a dac extension, create an unbound field (MyAvailability is the example field):
[PXTable(typeof(CROpportunityProducts.cROpportunityID), typeof(CROpportunityProducts.cROpportunityProductID), IsOptional = true)]
[Serializable]
public class CROpportunityProductsMyExtension : PXCacheExtension<CROpportunityProducts>
{
#region MyAvailability
public abstract class myAvailability : PX.Data.IBqlField
{
}
protected string _MyAvailability;
[PXString(IsUnicode = true)]
[PXUIField(DisplayName = "Product Availability", Enabled = false)]
public virtual string MyAvailability
{
get
{
return this._MyAvailability;
}
set
{
this._MyAvailability = value;
}
}
#endregion
}
(2) On the opportunity products tab, wire up the new field as the grid status value by setting property StatusField. The page needs modified to add this value and should look something like this when added (requires screen customization in your project -> actions edit ASPX and locate ProductsGrid to paste in your StatusField and value):
<px:PXGrid ID="ProductsGrid" SkinID="Details" runat="server" Width="100%"
Height="500px" DataSourceID="ds" ActionsPosition="Top" BorderWidth="0px"
SyncPosition="true" StatusField="MyAvailability">
(3) Now in a graph extension populate the field:
Edit: The use of Current<> does not always contain the correct currently highlighted row in the UI. Switched to Required<> based on the PXFieldSelectingEventArgs.Row and the results are correct for multiple rows in the products tab.
public class CROpportunityMaintMyExtension : PXGraphExtension<OpportunityMaint>
{
public virtual void CROpportunityProducts_MyAvailability_FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
var row = (CROpportunityProducts) e.Row;
if (row == null)
{
e.ReturnValue = string.Empty;
return;
}
INLocationStatus locationStatus = PXSelectGroupBy<INLocationStatus,
Where<INLocationStatus.inventoryID, Equal<Required<CROpportunityProducts.inventoryID>>,
And2<Where<INLocationStatus.subItemID, Equal<Required<CROpportunityProducts.subItemID>>,
Or<Not<FeatureInstalled<PX.Objects.CS.FeaturesSet.subItem>>>>,
And<Where<INLocationStatus.siteID, Equal<Required<CROpportunityProducts.siteID>>,
Or<Required<CROpportunityProducts.siteID>, IsNull>>>>>,
Aggregate<Sum<INLocationStatus.qtyOnHand, Sum<INLocationStatus.qtyAvail>>>
>.Select(sender.Graph, row.InventoryID, row.SubItemID, row.SiteID, row.SiteID);
// Need to convert to transaction UOM... (this is always base units)
decimal? qtyOnHand = locationStatus?.QtyOnHand;
decimal? qtyAvail = locationStatus?.QtyAvail;
e.ReturnValue = $"Qty On hand = {qtyOnHand.GetValueOrDefault()} ; Qty Avail = {qtyAvail.GetValueOrDefault()}";
}
}
The results: