I have two custom tables (Header / Detail) which have multiple fields as the key for both. I've created all the PXParent and PXDefault attributes in the detail select from the Header, just as shown in the T200 course.
The problem is that when I have the key fields on the header section and I go to try to create a new record, the form doesn't clear the values I've selected for those key fields.
I've gone on to just creating one auto-incrementing int identity field as the key, with the other former key fields assembled into a unique index instead. I'm using the PXDBIdentity(IsKey = true) attribute in my DAC - and it works well except for one problem: When I create a new record, the field shows -2147483647 until I save, when it then shows the newly created identity value.
So I guess my question is this:
What's the standard protocol / best practice for creating a key field for a custom table when the unique identity of that table consists of 6 fields? If I'm doing it correctly, how do I eliminate that -2147483647 from showing in the ID field?
There is only one major rule when it comes to defining key fields in a DAC:
only a field bound to an identity column can be the only key field in a DAC
you can define several key fields in a DAC as long as none of the defined key fields is bound to an identity column
The standard PXInsert action provided by the framework preserves values of all key fields, except the last one. If you want the Insert button to clear values of all key field, it's possible to inherit from the PXInsert<TNode> class and clear Searches array before base logic gets executed:
public class MyGraph : PXGraph<MyGraph>
{
public class PXInsertCst<TNode> : PXInsert<TNode>
where TNode : class, IBqlTable, new()
{
public PXInsertCst(PXGraph graph, string name)
: base(graph, name)
{
}
public PXInsertCst(PXGraph graph, Delegate handler)
: base(graph, handler)
{
}
[PXUIField(DisplayName = ActionsMessages.Insert,
MapEnableRights = PXCacheRights.Insert,
MapViewRights = PXCacheRights.Insert)]
[PXInsertButton]
protected override IEnumerable Handler(PXAdapter adapter)
{
adapter.Searches = null;
return base.Handler(adapter);
}
}
public PXSave<MyPrimaryDAC> Save;
public PXCancel<MyPrimaryDAC> Cancel;
// The standard PXInsert type was replaced with the custom PXInsertCst type
public PXInsertCst<MyPrimaryDAC> Insert;
public PXDelete<MyPrimaryDAC> Delete;
public PXCopyPasteAction<MyPrimaryDAC> CopyPaste;
public PXFirst<MyPrimaryDAC> First;
public PXPrevious<MyPrimaryDAC> Previous;
public PXNext<MyPrimaryDAC> Next;
public PXLast<MyPrimaryDAC> Last;
}
In case it's required to show the only DAC key field bound an identity column in the UI, but users don't want to see a temporary negative value generated for a new record, you should implement FieldSelecting and FieldUpdating event handlers for the DAC's identity column mapped field following the code sample below:
public class MyGraph : PXGraph<MyGraph>
{
protected void MyDAC_IdentityField_FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
if (e.Row != null && e.ReturnValue is int?)
{
if ((e.ReturnValue as int?).GetValueOrDefault() < 0)
{
e.ReturnValue = null;
}
}
}
protected void MyDAC_IdentityField_FieldUpdating(PXCache sender, PXFieldUpdatingEventArgs e)
{
if (e.Row != null && e.NewValue == null && sender.Inserted.Count() == 1)
{
var defaultValue = sender.GetValue<MyDAC.identityField>(sender.Inserted.FirstOrDefault_());
if (defaultValue != null)
{
e.NewValue = defaultValue;
}
}
}
}
For a more generic approach on the DAC level, you can implement a custom attribute inherited from PXDBIdentityAttribute and override FieldSelecting and FieldUpdating event handlers following the code sample below:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter |
AttributeTargets.Class | AttributeTargets.Method)]
public class PXDBNewIdentityAttribute : PXDBIdentityAttribute
{
public override void FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
base.FieldSelecting(sender, e);
if (e.Row != null && e.ReturnValue is int?)
{
if ((e.ReturnValue as int?).GetValueOrDefault() < 0)
{
e.ReturnValue = null;
}
}
}
public override void FieldUpdating(PXCache sender, PXFieldUpdatingEventArgs e)
{
if (e.Row != null && e.NewValue == null && sender.Inserted.Count() == 1)
{
var defaultValue = sender.GetValue(sender.Inserted.FirstOrDefault_(), FieldOrdinal);
if (defaultValue != null)
{
e.NewValue = defaultValue;
}
}
base.FieldUpdating(sender, e);
}
}
The problem with an identity field is that the number is generated by the Database and not by the ORM which generally holds new Inserted rows in the Cache before the record persists. My advice would be to either drop the identity from the UI altogether or figure out a different key.
Related
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.
how do I Enable usrsubcontractNbr in rowselected Event:
I am unable to access the usrSubcontractNbr from neither of the DAC (ApTran and ApTranExt)
UsrSubcontractNbr field was defined in construction feature package. APTran is converted into dll.
How can access this field?
Seems like a similar issue to this post: How to access Custom field,which is defined in Construction feature package- Acumatica
Searching the PX.Objects.CN.dll from the construction install package you will find:
using PX.Data;
using PX.Objects.AP;
using PX.Objects.CS;
namespace PX.Objects.CN.Subcontracts.AP.CacheExtensions
{
public sealed class ApTranExt : PXCacheExtension<APTran>
{
[PXString(15, IsUnicode = true)]
[PXUIField(DisplayName = "Subcontract Nbr.", Enabled = false, IsReadOnly = true)]
public string UsrSubcontractNbr
{
get
{
if (!(this.get_Base().get_POOrderType() == "RS"))
return (string) null;
return this.get_Base().get_PONbr();
}
}
[PXInt]
[PXUIField(DisplayName = "Subcontract Line", Enabled = false, IsReadOnly = true, Visible = false)]
public int? UsrSubcontractLineNbr
{
get
{
if (!(this.get_Base().get_POOrderType() == "RS"))
return new int?();
return this.get_Base().get_POLineNbr();
}
}
public static bool IsActive()
{
return PXAccess.FeatureInstalled<FeaturesSet.construction>();
}
public ApTranExt()
{
base.\u002Ector();
}
public abstract class usrSubcontractNbr : IBqlField, IBqlOperand
{
}
public abstract class usrSubcontractLineNbr : IBqlField, IBqlOperand
{
}
}
}
To access the field you will need to use PX.Objects.CN.Subcontracts.AP.CacheExtensions.ApTranExt
Edit. based on the comment if having an issue using rowselected be sure to use the signature with the PXRowSelected delegate so you can control when you enabled code to run after the base call. you might have a problem where the base call is running after your code which could disable the field again.
Ex:
protected void APTran_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected del)
{
del?.Invoke(cache, e);
var row = (APTran) e.Row;
if (row == null) return;
PXUIFieldAttribute.SetEnabled<PX.Objects.CN.Subcontracts.AP.CacheExtensions.ApTranExt.usrSubcontractNbr>(
cache, row, true);
}
I have a requirement to have a field on SalesOrder screen and the same field should appear on Shipment screen also for respective SalesOrder. And the user should be able to update these field on both the screen.
I created a bound field on Sales Order screen which user can save it. Then I created an unbound field on Shipment screen to show the text from Sales Order. For that I have written a SOShipment_RowSelected event and later for user to update it to the database, I have written SOShipment_RowUpdated. However, when I try to edit the field, it fires RowSelected event and it overwrites the editing and bring back in the same original value.
I have tried with SOShipment_ShipmentNbr_FieldUpdated & SOShipment_ShipmentNbr_FieldUpdating event but its not firing everytime.
Here is the code for Cache extension-
public class SOOrderExtension : PXCacheExtension<SOOrder>
{
#region UsrNotesText
[PXDBString(255)]
[PXUIField(DisplayName = "Pick List Notes")]
public virtual string UsrNotesText { get; set; }
public abstract class usrNotesText : IBqlField { }
#endregion
}
public class SOShipmentExtension : PXCacheExtension<SOShipment>
{
#region UsrNotesText
[PXString(255)]
[PXUIField(DisplayName = "Pick List Notes")]
public virtual string UsrNotesText { get; set; }
public abstract class usrNotesText : IBqlField { }
#endregion
}
SOShipmentExtension code-
public class SOShipmentEntryExtension : PXGraphExtension<SOShipmentEntry>
{
PXSelect<SOOrder> soOrder;
protected virtual void SOShipment_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
if (e.Row != null)
{
SOOrder order = PXSelectJoin<SOOrder,
LeftJoin<SOOrderShipment, On<SOOrder.orderNbr, Equal<SOOrderShipment.orderNbr>,
And<SOOrder.orderType, Equal<SOOrderShipment.orderType>>>,
LeftJoin<SOShipment, On<SOOrderShipment.shipmentNbr, Equal<SOShipment.shipmentNbr>>>>,
Where<SOShipment.shipmentNbr, Equal<Current<SOShipment.shipmentNbr>>>>.Select(Base);
if (order != null)
{
SOOrderExtension orderExt = PXCache<SOOrder>.GetExtension<SOOrderExtension>(order);
SOShipment soShipment = Base.Document.Current;
SOShipmentExtension ext = PXCache<SOShipment>.GetExtension<SOShipmentExtension>(soShipment);
ext.UsrNotesText = orderExt.UsrNotesText;
}
}
}
protected virtual void SOShipment_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
SOShipment oldRow = (SOShipment)e.OldRow;
SOShipment newRow = (SOShipment)e.Row;
if (oldRow != null || newRow != null)
{
SOShipmentExtension oldExt = PXCache<SOShipment>.GetExtension<SOShipmentExtension>(oldRow);
SOShipmentExtension newExt = PXCache<SOShipment>.GetExtension<SOShipmentExtension>(newRow);
if (oldExt.UsrNotesText != newExt.UsrNotesText)
{
{
SOOrder order = PXSelectJoin<SOOrder,
LeftJoin<SOOrderShipment, On<SOOrder.orderNbr, Equal<SOOrderShipment.orderNbr>,
And<SOOrder.orderType, Equal<SOOrderShipment.orderType>>>,
LeftJoin<SOShipment, On<SOOrderShipment.shipmentNbr, Equal<SOShipment.shipmentNbr>>>>,
Where<SOShipment.shipmentNbr, Equal<Current<SOShipment.shipmentNbr>>>>.Select(Base);
soOrder.Current = order;
if (order != null)
{
SOOrderExtension orderExt = PXCache<SOOrder>.GetExtension<SOOrderExtension>(order);
orderExt.UsrNotesText = newExt.UsrNotesText;
soOrder.Update(order);
}
}
}
}
}
}
Any suggestions?
The trick is to initialize UsrNotesText elsewhere.
You can use PXDefault attribute:
[PXDefault(typeof(Search<SOOrderExtension.usrNotesText, Where< [...] >>))]
Or FieldDefaulting event handler:
public void SOShipment_UsrNotesText_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
e.NewValue = [...]
}
Sometimes you also want to re-initialize when user changes key fields that are not re-triggering the default.:
public void SOShipment_ShipmentNbr_FieldUpdated(PXCache sender, PXFieldDefaultingEventArgs e)
{
SOShipment shipment = e.Row as SOShipment;
if (shipment != null)
{
SOShipmentExtension shipmentExt = PXCache<SOShipment>.GetExtension<SOShipmentExtension>(shipment);
if (shipmentExt != null)
{
shipmentExt.UsrNotesText = [...];
}
}
}
In such case manually re-triggering FieldDefaulting event with RaiseFieldDefaulting is often a better option.
However method you choose to initialize avoid setting the field value in RowSelected because that event is called at times when you don't want to initialize the custom field.
I have a custom field in AR Invoice and Memos (Screen ID AR301000) for the corresponding AP Ref. Nbr. And in the similar manager another custom field in AP Bills and Adjustment (Screen ID AP301000) for the corresponding AR Ref. Nbr.
I am trying to update AP Ref. Nbr. on AR screen when user updates the AR Ref. Nbr. in AP screen.
For example-
I am on AR Screen Invoice 0001, I am updating AP Ref. Nbr. to abc01. System will automatically update the AP Bill abc01 with the corresponding AR Ref. Nbr. with 0001.
I have below code written to achieve this but it runs into infinite loop as both it is trying to update the corresponding fields in other screen. Let me know if I missing anything or there is a better way.
On AR Graph Extension
public class ARInvoiceEntryExtension : PXGraphExtension<ARInvoiceEntry>
{
protected virtual void ARInvoice_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
var row = (ARInvoice)e.Row;
if (row != null && sender.IsDirty)
{
ARRegisterExtension ext = PXCache<ARRegister>.GetExtension<ARRegisterExtension>(row);
if (ext != null && !string.IsNullOrEmpty(ext.UsrAPRefNbr))
{
APInvoiceEntry graph = PXGraph.CreateInstance<APInvoiceEntry>();
APInvoice apRow = PXSelect<APInvoice,
Where<APInvoice.refNbr, Equal<Required<APInvoice.refNbr>>>>.Select(graph, ext.UsrAPRefNbr);
if (apRow != null)
{
APRegisterExtension ext1 = PXCache<APRegister>.GetExtension<APRegisterExtension>(apRow);
if (ext1 != null && string.IsNullOrEmpty(ext1.UsrARRefNbr)) //Update only if it is empty
{
ext1.UsrARRefNbr = row.RefNbr;
graph.Document.Current = apRow;
graph.Caches[typeof(APRegister)].SetValue<APRegisterExtension.usrARRefNbr>(apRow, row.RefNbr);
graph.Caches[typeof(APRegister)].Update(apRow);
graph.Actions.PressSave();
}
}
}
}
}
}
On AP Graph Extension
public class APInvoiceEntryExtension : PXGraphExtension<APInvoiceEntry>
{
protected virtual void APInvoice_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
var row = (APInvoice)e.Row;
if (row != null && sender.IsDirty)
{
APRegisterExtension ext = PXCache<APRegister>.GetExtension<APRegisterExtension>(row);
if (ext != null && !string.IsNullOrEmpty(ext.UsrARRefNbr))
{
ARInvoiceEntry graph = PXGraph.CreateInstance<ARInvoiceEntry>();
ARInvoice arRow = PXSelect<ARInvoice,
Where<ARInvoice.refNbr, Equal<Required<ARInvoice.refNbr>>>>.Select(graph, ext.UsrARRefNbr);
if (arRow != null)
{
ARRegisterExtension ext1 = PXCache<ARRegister>.GetExtension<ARRegisterExtension>(arRow);
if (ext1 != null && string.IsNullOrEmpty(ext1.UsrAPRefNbr)) //Update only if it is empty
{
ext1.UsrAPRefNbr = row.RefNbr;
graph.Document.Current = arRow;
graph.Caches[typeof(ARRegister)].SetValue<ARRegisterExtension.usrAPRefNbr>(arRow, row.RefNbr);
graph.Caches[typeof(ARRegister)].Update(arRow);
graph.Actions.PressSave();
}
}
}
}
}
}
AR Cache Extension
public class ARRegisterExtension : PXCacheExtension<ARRegister>
{
public abstract class usrAPRefNbr : PX.Data.IBqlField
{
}
protected string _usrAPRefNbr;
[PXDBString(15)]
[PXUIField(DisplayName = "AP Ref Nbr.", Visibility = PXUIVisibility.SelectorVisible)]
[APInvoiceType.RefNbr(typeof(Search3<PX.Objects.AP.Standalone.APRegisterAlias.refNbr,
InnerJoinSingleTable<APInvoice, On<APInvoice.docType, Equal<PX.Objects.AP.Standalone.APRegisterAlias.docType>,
And<APInvoice.refNbr, Equal<PX.Objects.AP.Standalone.APRegisterAlias.refNbr>>>,
InnerJoinSingleTable<Vendor, On<PX.Objects.AP.Standalone.APRegisterAlias.vendorID, Equal<Vendor.bAccountID>>>>,
OrderBy<Desc<APRegister.refNbr>>>))]
public virtual string UsrAPRefNbr
{
get; set;
}
}
AP Cache Extension
public class APRegisterExtension : PXCacheExtension<APRegister>
{
public abstract class usrARRefNbr : PX.Data.IBqlField
{
}
protected string _usrARRefNbr;
[PXDBString(15)]
[PXUIField(DisplayName = "AR Ref Nbr.", Visibility = PXUIVisibility.SelectorVisible)]
[ARInvoiceType.RefNbr(typeof(Search3<PX.Objects.AR.Standalone.ARRegisterAlias.refNbr,
InnerJoinSingleTable<ARInvoice, On<ARInvoice.docType, Equal<PX.Objects.AR.Standalone.ARRegisterAlias.docType>,
And<ARInvoice.refNbr, Equal<PX.Objects.AR.Standalone.ARRegisterAlias.refNbr>>>,
InnerJoinSingleTable<Customer, On<PX.Objects.AR.Standalone.ARRegisterAlias.customerID, Equal<Customer.bAccountID>>>>,
OrderBy<Desc<ARRegister.refNbr>>>))]
public virtual string UsrARRefNbr
{
get; set;
}
}
When saving, APInvoice_RowUpdated modifies ARInvoice which in turn modifies APInvoice which triggers APInvoice_RowUpdated producing an infinite loop of event calls. The inverse in ARInvoice_RowUpdated which updates APInvoice will result in a similar infinite loop.
To get out of this, you can remove the graph event handler at runtime after instantiating the graph. First make your event handler access modifier public so you can reference them:
public virtual void APInvoice_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
After creating a graph, get the extension and remove the handler that cause the infinite loop:
APInvoiceEntry graph = PXGraph.CreateInstance<APInvoiceEntry>();
APInvoiceEntryExtension graphExt = graph.GetExtension<APInvoiceEntryExtension>();
graphExt.RowUpdated.RemoveHandler<APInvoice>(graphExt.APInvoice_RowUpdated);
The same modification has to be done for ARInvoice because the infinite loop goes both way, from AP to AR and from AR to AP.
I want to add the attributes for my inventory lookup on Sales Order and Purchase Order, does anyone know how to? or any ideas?
Please refer below code example to include Attributes Columns in Selector and Grid Control utilizing out-of-box CRAttributesFieldAttribute.
Declare a class PXAddAtttributeColumns inherited from CRAttributesFieldAttribute.
public class PXAddAtttributeColumns : CRAttributesFieldAttribute
{
string[] _names;
bool _IsForSelector;
public PXAddAtttributeColumns(string[] names, Type entityType, Type entityIDField, Type classIDField, bool IsForSelector = true)
: base(entityType, entityIDField, classIDField)
{
_names = names;
_IsForSelector = IsForSelector;
}
public override void CacheAttached(PXCache sender)
{
this._IsActive = true;
base.CacheAttached(sender);
}
protected override void AttributeFieldSelecting(PXCache sender, PXFieldSelectingEventArgs e, PXFieldState state, string attributeName, int idx)
{
if (_names.Any(attributeName.Equals))
{
//Out-of-box DisplayName is prefixed with "$Attributes$-" - if you need to take that off.
state.DisplayName = (!String.IsNullOrEmpty(state.DisplayName)) ? (state.DisplayName.Replace("$Attributes$-", "")) : attributeName;
state.Visible = true;
//Requires AutoGenerateColumns="AppendDynamic" for PXGrid Control for dynamic Attribute columns creation
state.Visibility = (_IsForSelector) ? PXUIVisibility.SelectorVisible : PXUIVisibility.Dynamic;
}
base.AttributeFieldSelecting(sender, e, state, attributeName, idx);
}
public override void CommandPreparing(PXCache sender, PXCommandPreparingEventArgs e)
{
base.CommandPreparing(sender, e);
if (e.BqlTable == null && aggregateAttributes && sender.GetItemType().IsDefined(typeof(PXProjectionAttribute), true))
{
e.BqlTable = _BqlTable;
}
}
}
To include attributes as Columns in Inventory Look up, declare DAC Extension as below:
public class InventoryItemPXExt : PXCacheExtension<PX.Objects.IN.InventoryItem>
{
#region Attributes
public abstract class attributes : IBqlField { }
[PXAddAtttributeColumns(new[] { "ASSETID", "HWMODEL" },
typeof(CSAnswerType.inventoryAnswerType),
typeof(InventoryItem.inventoryID),
typeof(InventoryItem.itemClassID))]
public virtual string[] Attributes { get; set; }
#endregion
}
Fields will show up as below:
Search can be Enabled on Attribute Columns by setting FilterByAllFields to True
To include attributes as Columns in Sales Order Details Grid, declare DAC Extension as below:
public class SOLineExtension : PXCacheExtension<SOLine>
{
public abstract class itemAttributes : IBqlField { }
[PXAddAtttributeColumns(new[] { "ASSETID", "HWMODEL" },
typeof(CSAnswerType.inventoryAnswerType),
typeof(SOLine.inventoryID),
typeof(InventoryItem.itemClassID), false)]
public virtual string[] ItemAttributes { get; set; }
}
Make sure to specify AutoGenerateColumns="AppendDynamic" for PXGrid control dynamic Attribute columns creation
Fields will show up as below:
To include attributes as Columns in grid of Add Stock Item Dialog, declare DAC Extension as below:
public class SOSiteStatusSelectedExtension : PXCacheExtension<SOSiteStatusSelected>
{
public abstract class itemAttributes : IBqlField { }
[PXAddAtttributeColumns(new[] { "ASSETID", "HWMODEL" },
typeof(CSAnswerType.inventoryAnswerType),
typeof(InventoryItem.inventoryID),
typeof(InventoryItem.itemClassID), false)]
public virtual string[] ItemAttributes { get; set; }
}
Make sure to specify AutoGenerateColumns="AppendDynamic" for PXGrid control dynamic Attribute columns creation
Fields will show up as below:
Note: This example is applicable to 5.3 series – Build 5.30.1367 onwards.
Updated solution for versions later than 20R1:
Below is the sample scenario to add custom attributes to both CustomerID and InventoryID lookups of Sales Orders (SO301000) screen.
Create the required attributes from Attributes (CS205000) screen.
Add the necessary attribute to the corresponding Customer Classes (AR201000) screen to be added to the CustomerID lookup and to the corresponding Item Classes (IN201000) screen to be added to the InventoryID lookup.
Create a new Attribute class CustomerAttributeSetFieldsAttribute extending CustomerActiveAttribute (since it is used in CustomerID) and override SetColumns by rewriting the same in order to change the FieldList and HeaderList.
public class CustomerAttributeSetFieldsAttribute : CustomerActiveAttribute
{
public static void SetColumns(PXCache cache, string field, string[] fieldList, string[] headerList)
{
PXSelectorAttribute.SetColumns(cache, field, fieldList, headerList);
foreach (CustomerAttributeSetFieldsAttribute attr in cache.GetAttributes(field).OfType<CustomerAttributeSetFieldsAttribute>())
{
attr._FieldList = fieldList;
attr._HeaderList = headerList;
}
}
}
Override Initialize method and call Activate for activating the attribute by passing proper cache - BAccountR - for CustomerID and InventoryItem - for InventoryID. Also, call SetColumns from the custom attribute for CustomerID CustomerAttributeSetFieldsAttribute and PXSelectorAttribute for InventoryID and pass the cache type (where the lookup is present), field (field name), fieldList (list of names of all the fields including the attributes) and headerList (list of names of all the headers including the attributes).
Override the attributes to merge the newly added attribute to CustomerID by adding CustomerAttributeSetFields to SOOrder_CustomerID_CacheAttached method.
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public override void Initialize()
{
base.Initialize();
//for InventoryID lookup
CR.CRAttributesFieldAttribute.Activate(Base.Caches[typeof(InventoryItem)]);
PXSelectorAttribute.SetColumns(Base.Caches[typeof(SOLine)],
"InventoryID", new string[] { "InventoryCD", "Descr", "itemClassID", "Color_ATTRIBUTES" },
new string[] { "InventoryCD", "Descr", "itemClassID", "$Attributes$-Color" });
//for CustomerID lookup
CR.CRAttributesFieldAttribute.Activate(Base.Caches[typeof(BAccountR)]);
CustomerAttributeSetFieldsAttribute.SetColumns(Base.Caches[typeof(SOOrder)],
"CustomerID", new string[] { "AcctCD", "acctName", "COMPREV_Attributes" },
new string[] { "AcctCD", "acctName", "$Attributes$-Company Revenue" });
}
[PXMergeAttributes(Method = MergeMethod.Merge)]
[CustomerAttributeSetFields]
protected virtual void SOOrder_CustomerID_CacheAttached(PXCache sender)
{
}
}
Note: Since Customer DAC is derived from BAccount and CustomerActive attribute is derived from Customer, it has the cache type BAccountR and only the attributes which are of the same cache type are available to add to the CustomerID popup. If the user needs to add other attributes of different cache type, it will be better to create a new Attribute and use that instead of CustomerActive.