I have segmented INLocation.LocationCD and need to restrict the PXSelector to include or exclude a particular segment based on the screen where the location is being entered. I have been able to access the DAC extension field in the displayed columns of the PXSelector, but the field is evaluated as null during the Search<> parameter of PXSelector.
I have tried:
referencing INLocationExt.myField directly,
making an inherited DAC to define the custom field directly,
and creating a PXProjection in hopes that the additional layer would cause the unbound field to be retrieved to populate the PXSelector in case the field was not being loaded in time for the Search<>.
Key points:
This is an unbound field in the DAC extension
It returns an value based evaluating the last segment of the INLocation.LocationCD
Generic Inquiries properly display this value
I have been unable to make PXSelector return any values when I reference the unbound field unless I simply check And
The field defined in my DAC extension:
[PXString(1)]
[PXUIField(DisplayName = "Condition")]
[ConditionType.List]
public String UsrSSCondition
{
get
{
if (LocationCD == null || LocationCD.Length == 0) return ConditionType.Undefined;
switch (LocationCD.Substring(LocationCD.Length - 1, 1))
{
case "N":
return ConditionType.New;
case "R":
return ConditionType.Repair;
case "C":
return ConditionType.Core;
case "U":
return ConditionType.Used;
default:
return ConditionType.Undefined;
}
}
}
public abstract class usrSSCondition : PX.Data.BQL.BqlString.Field<usrSSCondition> { }
The PXSelector:
[PXSelector(typeof(Search<INLocation.locationID, Where<INLocation.receiptsValid, Equal<True>,
And<INLocationExt.usrSSCondition, NotEqual<ConditionType.core>>>>),
typeof(INLocation.locationCD),
typeof(INLocation.active),
typeof(INLocation.primaryItemID),
typeof(INLocation.primaryItemClassID),
typeof(INLocationExt.usrSSCondition),
typeof(INLocation.receiptsValid),
SubstituteKey = typeof(INLocation.locationCD))]
The PXProjection:
[Serializable]
[PXCacheName("SSCS INLocation")]
[PXProjection(typeof(Select<INLocation>))]
public partial class SSINLocation : IBqlTable
{
#region LocationID
[PXDBInt(IsKey = true, BqlField = typeof(INLocation.locationID))]
public int? LocationID { get; set; }
public abstract class locationID : PX.Data.BQL.BqlInt.Field<locationID> { }
#endregion
#region LocationCD
[PXDBString(BqlField = typeof(INLocation.locationCD))]
public String LocationCD { get; set; }
public abstract class locationCD : PX.Data.BQL.BqlString.Field<locationCD> { }
#endregion
#region UsrSSCondition
[PXDBString(BqlField = typeof(INLocationExt.usrSSCondition))]
public String UsrSSCondition { get; set; }
public abstract class usrSSCondition : PX.Data.BQL.BqlString.Field<usrSSCondition> { }
#endregion
#region ReceiptsValid
[PXDBBool(BqlField = typeof(INLocation.receiptsValid))]
public bool? ReceiptsValid { get; set; }
public abstract class receiptsValid : PX.Data.BQL.BqlBool.Field<receiptsValid> { }
#endregion
#region Active
[PXDBBool(BqlField = typeof(INLocation.active))]
public bool? Active { get; set; }
public abstract class active : PX.Data.BQL.BqlBool.Field<active> { }
#endregion
#region PrimaryItemID
[PXDBInt(BqlField = typeof(INLocation.primaryItemID))]
public int? PrimaryItemID { get; set; }
public abstract class primaryItemID : PX.Data.BQL.BqlInt.Field<primaryItemID> { }
#endregion
#region PrimaryItemClassID
[PXDBInt(BqlField = typeof(INLocation.primaryItemClassID))]
public int? PrimaryItemClassID { get; set; }
public abstract class primaryItemClassID : PX.Data.BQL.BqlInt.Field<primaryItemClassID> { }
#endregion
}
I have tried simple versions and various combinations of those to no avail. How do I leverage my "condition" in the Search<> clause of the PXSelector?
Edit 1:
Picture of PXSelector returning values as a column - does not work as a PXRestrictor or as a where clause in Select<>.
Edit 2: More info
I have simplified the DAC extension to use PXFormula and pick out the last character of the LocationCD retrieved by PXFormula to set the value.
We need to use the last segment of the LocationCD to manage the condition of the part in a bin. This would allow us to separate cost as well as manage MRO Spares for maintenance as New, Used, Repaired, and Requiring Repair while also allowing the ability to specify other conditions later (like NCM if received damaged, etc.) if needed. While some materials can be used globally, materials of certain conditions need to be available under certain use cases. My intended strategy is to apply the rules to the last segment of the Location CD to allow the PXSelector to control user entry, either as a DAC extension on INLocation or as a Cache_Attached in the relevant graphs, if necessary.
I created a DAC extension on INLocation for usrSSCondition as a PXString. My latest attempt was to use a PXFormula to pull the LocationCD value and then custom code on the set{} to pick out the last segment and set the code for the relevant condition. (This technique was actually new to me, and a response in the stackoverflow post guided me to the idea.)
When using in a PXSelector as a displayed column, I can see the value. However, the Select<> does not allow me to tap into that segment or custom PXString field used to display that condition. I was hoping that some "behind the scenes magic" would evaluate my PXString field to limit the results, but it seems that the field is returned as null during the Select and then processed in a later step of DAC processing. When I think about what a Select is doing, it would make sense that data not stored in the database cannot be used to filter the results. PXRestrictor does not impact it either.
1) Is there a way to get my DAC to process the PXString value before the PXSelector applies the where clause?
2) Is this something I need to take to an attribute for post-processing? (If so, any suggestions on where to look for a simple example?)
Updated DAC:
#region usrSSCondition
private String _condition;
[PXString]
[PXUIField(DisplayName = "Condition")]
[PXFormula(typeof(INLocation.locationCD))]
[ConditionType.List]
public String UsrSSCondition
{
get { return _condition; }
set
{
string Loc = value;
if (Loc == null || Loc.Length == 0)
{
_condition = ConditionType.Undefined;
}
else
{
_condition = (Loc.Substring(Loc.Length - 1, 1)) switch
{
"N" => ConditionType.New,
"R" => ConditionType.Repair,
"C" => ConditionType.Core,
"U" => ConditionType.Used,
_ => ConditionType.Undefined,
};
}
}
}
Do not ever use code inside the getter, it won't be work properly in BQL expressions!
If you wanna check Loc.Substring(Loc.Length - 1, 1) somewhere in BQL just write your own BQL function
public class ConditionTypeBySegment<Source> : BqlFunction, IBqlOperand, IBqlCreator
where Source : IBqlOperand
{
private IBqlCreator _source;
public void Verify(PXCache cache, object item, List<object> pars, ref bool? result, ref object value)
{
if (!getValue<Source>(ref _source, cache, item, pars, ref result, out value) || value == null)
return;
if (value is string strValue)
{
switch (strValue.Substring(strValue.Length - 1, 1))
{
case "N":
value = ConditionType.New;
break;
case "R":
value = ConditionType.Repair;
break;
case "C":
value = ConditionType.Core;
break;
case "U":
value = ConditionType.Used;
break;
default:
value = ConditionType.Undefined;
break;
}
return;
}
value = ConditionType.Undefined;
}
public bool AppendExpression(ref SQLExpression exp, PXGraph graph, BqlCommandInfo info, BqlCommand.Selection selection)
{
...
return true;
}
}
or use a combination of existing functions. For example:
[PXSelector(typeof(Search<INLocation.locationID,
Where<INLocation.receiptsValid, Equal<True>,
And<Substring<FABookBalance.deprToPeriod, Sub<StrLen<FABookBalance.deprToPeriod>, int1>, int1>, NotEqual<ConditionTypes.tCore>>>>),
typeof(INLocation.locationCD),
typeof(INLocation.active),
typeof(INLocation.primaryItemID),
typeof(INLocation.primaryItemClassID),
typeof(INLocationExt.usrSSCondition),
typeof(INLocation.receiptsValid),
SubstituteKey = typeof(INLocation.locationCD))]
public static class ConditionTypes
{
public class tNew : PX.Data.BQL.BqlString.Constant<tNew> { public tNew() : base("N") { } }
public class tRepair : PX.Data.BQL.BqlString.Constant<tRepair> { public tRepair() : base("R") { } }
public class tCore : PX.Data.BQL.BqlString.Constant<tCore> { public tCore() : base("C") { } }
public class tUsed : PX.Data.BQL.BqlString.Constant<tUsed> { public tUsed() : base("U") { } }
}
Simple solution - simplify. Changed the field to simply hold the value and then let the BLC set the value when the locaitonCD value is set. On creation of the record, the locationCD field is empty, so defining the FieldDefaulting logic causes the condition to be undefined initially. By monitoring FieldUpdated of the LocationCD, we can then reapply the FieldDefaulting rules to the "real" value.
DAC field definition:
#region usrSSCondition
[PXDBString]
[PXUIField(DisplayName = "Condition")]
[ConditionType.List]
public String UsrSSCondition { get; set; }
public abstract class usrSSCondition : PX.Data.BQL.BqlString.Field<usrSSCondition> { }
#endregion
Event Handlers in the BLC:
#region INLocationExt_UsrSSCondition_FieldDefaulting
protected void INLocation_UsrSSCondition_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
INLocation row = (INLocation)e.Row;
string Loc = row?.LocationCD;
if (Loc == null || Loc.Length == 0)
{
e.NewValue = ConditionType.Undefined;
}
else
{
e.NewValue = (Loc.Substring(Loc.Length - 1, 1)) switch
{
ConditionType.New => ConditionType.New,
ConditionType.Repair => ConditionType.Repair,
ConditionType.Core => ConditionType.Core,
ConditionType.Used => ConditionType.Used,
_ => ConditionType.Undefined,
};
}
}
#endregion
#region INLocation_LocationCD_FieldUpdated
protected void _(Events.FieldUpdated<INLocation.locationCD> e)
{
INLocation row = (INLocation)e.Row;
e.Cache.SetDefaultExt<INLocationExt.usrSSCondition>(row);
}
#endregion
Since locations are defined in INSiteMaint, the event handlers in that graph allow setting the field value to store in the database without any translation. That enables use of PXRestrictorAttribute to limit available locations accordingly or write rules to set the location flags on the INSiteMaint screen.
Below is one example of CacheAttached to add a PXRestrictor to prevent receipt into a Core location type unless it is done from the NcmTag Screen. (A heavy hand controlling what locations a user may select is not needed universally, so this was not applied globally to the DAC field.)
#region INTran_LocationID_CachedAttached
[PXMergeAttributes(Method = MergeMethod.Append)]
[PXRestrictor(typeof(Where<INLocationExt.usrSSCondition, NotEqual<ConditionType.core>,
Or<Current<AccessInfo.screenID>, Equal<SSCS.Constants.NcmTagScreenID>>>), "")]
#endregion
Also worth noting, since my purpose is to use a character of the LocationCD for the end user to recognize the location type, I have to prevent the user from changing the LocationCD value through the RowSelected event for INLocation.
#region INLocation_RowSelected
protected void _(Events.RowSelected<INLocation> e)
{
INLocation row = e.Row;
if(row?.SiteID != null)
{
INLocationExt rowExt = row.GetExtension<INLocationExt>();
PXUIFieldAttribute.SetEnabled<INLocation.locationCD>(e.Cache, row,
!DisableLocationRename(row?.LocationID));
}
}
#endregion
#region DisableLocationRename
protected virtual bool DisableLocationRename(int? locationID)
{
int counter = PXSelect<SSINNcmTag,
Where<SSINNcmTag.locationID, Equal<Required<SSINNcmTag.locationID>>,
And<SSINNcmTag.tranRefNbr, IsNull>>>
.SelectSingleBound(Base, null, locationID).Count;
if (counter > 0) return true;
counter = PXSelect<INLocationStatus,
Where <INLocationStatus.locationID, Equal<Required<INLocationStatus.locationID>>,
And<INLocationStatus.qtyOnHand, Greater<DecimalZero>>>>
.SelectSingleBound(Base, null, locationID).Count;
if (counter > 0) return true;
return false;
}
#endregion
While we have the ability to write some very interesting code, it is important to stop once in a while to ask, "Why am I making this complicated?" Whenever possible, simplify.
Good day
Page: AP503000
Build 19.104.0024
I am trying to calculate the Total Amount paid for all recorded in the APAdjust grid on the bottom of the Prepare Payments. This total I want to show on top in the Form area.
DAC
namespace PX.Objects.AP
{
public class PayBillsFilterExt : PXCacheExtension<PX.Objects.AP.PayBillsFilter>
{
#region UsrTotalAmountForPayment
[PXDecimal]
[PXUIField(DisplayName = "Total Amount For Payment")]
public virtual Decimal? UsrTotalAmountForPayment { get; set; }
public abstract class usrTotalAmountForPayment :
PX.Data.BQL.BqlDecimal.Field<usrTotalAmountForPayment> { }
#endregion
}
}
My first attempt I tried to add an unbound formula to my DAC for the below to work you will also need to add a new class decimal_0
namespace PX.Objects.AP
{
public class PayBillsFilterExt : PXCacheExtension<PX.Objects.AP.PayBillsFilter>
{
#region UsrTotalAmountForPayment
[PXDecimal]
[PXUIField(DisplayName = "Total Amount For Payment")]
// add this to the DAC
[PXUnboundFormula(typeof(Where<APAdjust.curyAdjdAmt,Greater<decimal_0>>)
,typeof(SumCalc<APAdjust.curyAdjdAmt>))]
public virtual Decimal? UsrTotalAmountForPayment { get; set; }
public abstract class usrTotalAmountForPayment :
PX.Data.BQL.BqlDecimal.Field<usrTotalAmountForPayment> { }
#endregion
//Class decimal_0
public class decimal_0 : Constant<decimal>
{
public decimal_0()
: base(0)
{ }
}
}
}
I have also tried adding an event to do the calculation:
namespace PX.Objects.AP
{
public class APPayBills_Extension : PXGraphExtension<APPayBills>
{
#region Event Handlers
protected void APAdjust_RowInserted(PXCache cache, PXRowInsertedEventArgs e)
{
var row = (APAdjust)e.Row;
PayBillsFilter filter = this.Base.Filter.Current;
PayBillsFilterExt FilExt = PXCache<PayBillsFilter>.GetExtension<PayBillsFilterExt>(filter);
if (filter != null)
{
FilExt.UsrTotalAmountForPayment += row.CuryAdjdAmt;
}
}
}
}
For accurate results I recommend you sum all the records at the same time.
You can use the DataView.Select method to iterate all records.
public void PayBillsFilter_UsrTotalAmountForPayment_FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
decimal total = 0;
foreach (APAdjust adjustment in this.Base.APDocumentList.Select())
{
total += (adjustment.CuryAdjdAmt != null ? adjustment.CuryAdjdAmt.Value : 0M);
}
e.ReturnValue = total;
}
I am working on the Project Budget Screen of acumatica, the screen uses the table PMProjectStatusEx which is a Projection table of PMProjectStatus. So I extended the PMProjectStatus table and added a field in there, I also extended the PMProjectStatusEx to add the same field and added it to the screen. But unlike the standard fields that updates the physical table PMProjectStatus my added field does not update the physical table. What could be the reason for this? Below is my code
Thanks
public class PMProjectStatusExt :
PXCacheExtension<PX.Objects.PM.PMProjectStatus>
{
#region UsrMarkupPct
public abstract class usrMarkupPct : PX.Data.IBqlField
{
}
protected Decimal? _UsrMarkupPct;
[PXDBDecimal(6, MinValue = 0, MaxValue = 1000)]
//[PXDefault(TypeCode.Decimal, "0.0")]
[PXUIField(DisplayName = "Markup %")]
public virtual Decimal? UsrMarkupPct
{
get
{
return this._UsrMarkupPct;
}
set
{
this._UsrMarkupPct = value;
}
}
#endregion
public class PMProjectStatusExExt :
PXCacheExtension<PX.Objects.PM.PMProjectStatusEx>
{
#region UsrMarkupPct
public abstract class usrMarkupPct : PX.Data.IBqlField
{
}
protected Decimal? _UsrMarkupPct;
[PXDBDecimal(6, MinValue = 0, MaxValue = 1000, BqlField = typeof(PMProjectStatusExt.usrMarkupPct))]
[PXDefault(TypeCode.Decimal, "0.0")]
[PXUIField(DisplayName = "Markup %")]
public virtual Decimal? UsrMarkupPct
{
get
{
return this._UsrMarkupPct;
}
set
{
this._UsrMarkupPct = value;
}
}
#endregion
When you add fields using the DATA ACCESS section of the Project Editor it will generate DB Scripts to update the table behind the scene:
When you add fields using a DAC extension in CODE section, it will not generate the DB Scripts.
In that case you need to manually add the scripts in DB Scripts section.
EDIT
One way to manually add DB field in DB Scripts is:
IF NOT EXISTS(SELECT * FROM Sys.Columns WHERE Name = N'UsrMarkupPct' and
Object_ID = Object_ID(N'PMProjectStatus'))
BEGIN
ALTER TABLE PMProjectStatus ADD UsrMarkupPct DECIMAL(19,6)
END
GO
I have extended a APTran DAC for Bills and Adjustments screen (ID - AP301000).
I am trying to populate a value from different table based on the current line item.
The value I need is from CrossReference based on current Inventory on line item and VendorID of current Bill.
Below is the code. Please let me know if I am missing anything.
public class string_VendorType : Constant<string>
{
public string_VendorType() : base("0VPN")
{ }
}
protected string _UsrVendorPartNum;
[PXString(50)]
[PXUIField(DisplayName = "Vendor Part Number", Enabled = false, IsReadOnly = true)]
[PXDefault(typeof(Search2<INItemXRef.alternateID,
LeftJoin<InventoryItem, On<INItemXRef.inventoryID, Equal<InventoryItem.inventoryID>>,
LeftJoin<APTran, On<InventoryItem.inventoryID, Equal<APTran.inventoryID>>,
LeftJoin<APInvoice, On<APInvoice.refNbr, Equal<APTran.refNbr>,
And<APInvoice.vendorID, Equal<INItemXRef.bAccountID>>>>>>,
Where<InventoryItem.inventoryID, Equal<Current<APTran.inventoryID>>,
And<INItemXRef.alternateType, Equal<string_VendorType>,
And<APInvoice.refNbr, Equal<Current<APTran.refNbr>>>>>>))]
public virtual string UsrVendorPartNum
{
get
{
return _UsrVendorPartNum;
}
set
{
_UsrVendorPartNum = value;
}
}
public abstract class usrVendorPartNum : IBqlField { }
However, the value isn't populating. Please advise.
I got the following working (using PXUnboundDefault). The query you have can be simplified to the following working example:
public class APTranExt : PXCacheExtension<PX.Objects.AP.APTran>
{
protected string _UsrVendorPartNum;
[PXString(50)]
[PXUIField(DisplayName = "Vendor Part Number", Enabled = false, IsReadOnly = true)]
[PXUnboundDefault(typeof(Search<INItemXRef.alternateID,
Where<INItemXRef.inventoryID, Equal<Current<APTran.inventoryID>>,
And<INItemXRef.alternateType, Equal<INAlternateType.vPN>,
And<INItemXRef.bAccountID, Equal<Current<APTran.vendorID>>>>>>))]
public virtual string UsrVendorPartNum
{
get
{
return _UsrVendorPartNum;
}
set
{
_UsrVendorPartNum = value;
}
}
public abstract class usrVendorPartNum : IBqlField { }
}
Note that you do not need to create your own constant. You can reuse INAlternateType and the vPN constant.
Simply for reference...
I would say if this was a DB field you could look into using the AlternativeItemAttribute, however it requires a subitem field which oddly APTran does not have in it.
Example usage of AlternativeItemAttribute on POLine.AlternateID:
public abstract class alternateID : PX.Data.IBqlField
{
}
protected String _AlternateID;
[AlternativeItem(INPrimaryAlternateType.VPN, typeof(POLine.vendorID), typeof(POLine.inventoryID), typeof(POLine.subItemID))]
public virtual String AlternateID
{
get
{
return this._AlternateID;
}
set
{
this._AlternateID = value;
}
}
How can I translate this SQL query:
SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name
HAVING aggregate_function(column_name) operator value
to Acumatica BQL?
I believe there is no Having function available in Bql yet.
The way I have solved this is with projections to generate sub-queries. For example:
[Serializable]
[PXProjection(typeof(
Select4<Table,
Aggregate<
GroupBy<Table.column1,
Sum<Table.column2>>>>))]
public class AggregateTable : IBqlTable
{
// This will contain aggregate value - GroupBy
public abstract class column1: PX.Data.IBqlField
{
}
[PXDBString(BqlField = typeof(Table.column1))]
[PXUIField(DisplayName = "Column1")]
public virtual string Column1 { get; set; }
// This will contain aggregate value - Sum
public abstract class column2: PX.Data.IBqlField
{
}
[PXDBInt(BqlField = typeof(Table.column2))]
[PXUIField(DisplayName = "Column2")]
public virtual int? Column2 { get; set; }
}
Then you can filter the aggregate with a normal select:
public PXSelect<AggregateTable, Where<AggregateTable.column2, Equal<Required<AggregateTable.column2>>>> FilteredTable;
var rows = FilteredTable.Select(10);
I try to use this very sparingly because it can get messy if you are not careful.
In FBQL appeared Having. Below goes example of AggregateTo<> and OrderBy<> sections with Having implemented:
.AggregateTo<Sum<field1>, GroupBy<field2>, Max<field3>,
Min<field4>, Avg<field5>, Count<field6>>.
Having<field5.Averaged.IsGreater<Zero>>
.OrderBy<field1.Asc, field2.Desc, field3.Asc>