Skip to main content

Hi Team,

I need to make changes in the selector of the 'Labor Items' field in the 'Employee Time Card' screen's details tab, based on the earning type I am selecting. Kindly assist me in how I can customize this. Please find attached images.

 

If the earning type = RG, then Labor Items displayed should not include those that begin with X. If Earning Type <> "RG, the Labor Items displayed should be only whose that begin with X.

 

We will wait for your response.

Regards

Sagar

I spent enough time on this that I feel like I should reply, but also, I really don't have anything to show for it. It seems like a PXRestrictor would do the trick, but I haven't been able to work out the logic. I also tried extending the PMLaborItemAttribute and overriding the selector results, but I believe it's a PXDimensionSelector at its core, and those are so much more complicated. I'm sure someone here could help, if they had the time. You may have to seek paid assistance 🙂

@zfebert56 @Keith Richardson 


I have done something similar with SOOrder DefaultSiteID. Ran into the same issue with Restrictors. Our logic restricts specific warehouses based on the sales branch, as we have 22+ locations, and truck deliveries to customers must come out of their branch. To accomplish this, we have a table that is linked to branch that shows what sites can be delivered out of. The goal was to change the selector based on the branch changing, not just the field validating.

What I did, was copied the base attribute to a new attribute. The old was SiteAttribute, and made one called HWDEDeliveryWarehouseSiteAttribute.

Inside that attribute, the constructor has this following line that will be updated. I am posting the initial constructor for reference too.

            Type type = BqlCommand.Compose(list.ToArray());


public SiteAttribute(Type WhereType, bool validateAccess, bool allowTransit)
{
if (!(WhereType != null))
{
return;
}

_whereType = WhereType;
List<Type> list = new List<Type>();
if (validateAccess)
{
list.Add(typeof(Search<,>));
list.Add(typeof(INSite.siteID));
list.Add(typeof(Where2<,>));
list.Add(typeof(Match<>));
list.Add(typeof(Current<AccessInfo.userName>));
if (allowTransit)
{
list.Add(typeof(And<>));
}
else
{
list.Add(typeof(And<,,>));
list.Add(typeof(INSite.siteID));
list.Add(typeof(NotEqual<transitSiteID>));
list.Add(typeof(And<>));
}

list.Add(_whereType);
}
else
{
list.Add(typeof(Search<,>));
list.Add(typeof(INSite.siteID));
if (!allowTransit)
{
list.Add(typeof(Where2<,>));
list.Add(typeof(Where<,>));
list.Add(typeof(INSite.siteID));
list.Add(typeof(NotEqual<transitSiteID>));
list.Add(typeof(And<>));
}

list.Add(_whereType);
}

Type type = BqlCommand.Compose(list.ToArray());
PXDimensionSelectorAttribute pXDimensionSelectorAttribute;
_Attributes.Add(pXDimensionSelectorAttribute = new PXDimensionSelectorAttribute("INSITE", type, typeof(INSite.siteCD), typeof(INSite.siteCD), typeof(INSite.descr)));
pXDimensionSelectorAttribute.CacheGlobal = true;
pXDimensionSelectorAttribute.DescriptionField = typeof(INSite.descr);
_SelAttrIndex = _Attributes.Count - 1;
}


My updated code for this function is

 


public HWDEDeliveryWarehouseSiteAttribute(Type WhereType, bool validateAccess, bool allowTransit)
{
if (WhereType != null)
{
_whereType = WhereType;

List<Type> bql = new List<Type>();

if (validateAccess)
{
bql.Add(typeof(Search<,>));
bql.Add(typeof(INSite.siteID));
bql.Add(typeof(Where2<,>));
bql.Add(typeof(Match<>));
bql.Add(typeof(Current<AccessInfo.userName>));
if (allowTransit)
{
bql.Add(typeof(And<>));
}
else
{
bql.Add(typeof(And<,,>));
bql.Add(typeof(INSite.siteID));
bql.Add(typeof(NotEqual<transitSiteID>));
bql.Add(typeof(And<>));
}
bql.Add(_whereType);
}
else
{
bql.Add(typeof(Search<,>));
bql.Add(typeof(INSite.siteID));
if (!allowTransit)
{
bql.Add(typeof(Where2<,>));
bql.Add(typeof(Where<,>));
bql.Add(typeof(INSite.siteID));
bql.Add(typeof(NotEqual<transitSiteID>));
bql.Add(typeof(And<>));
}
bql.Add(_whereType);
}

//Type SearchType = BqlCommand.Compose(bql.ToArray());
Type SearchType = typeof(Search2<INSite.siteID,
LeftJoin<HWDEDeliveryWarehouse, On<HWDEDeliveryWarehouse.siteID, Equal<INSite.siteID>, And<HWDEDeliveryWarehouse.branchID, Equal<Current<SOOrder.branchID>>>>,
LeftJoin<Carrier, On<Carrier.carrierID, Equal<Current<SOOrder.shipVia>>>,
LeftJoin<SOOrderType, On<SOOrderType.orderType, Equal<Current<SOOrder.orderType>>>>>>,
Where2<
Where<INSite.siteID, NotEqual<SiteAttribute.transitSiteID>,
And<Match<Current<AccessInfo.userName>>>>,
And<Where2<
Where<HWDEDeliveryWarehouse.branchID, IsNotNull,
And<CarrierExt.usrHWDELockDeliveryWarehouse, Equal<True>,
And<SOOrderTypeExt.usrHWDEEnableDeliveryRestrictions, Equal<True>>>>,
Or<Where<
CarrierExt.usrHWDELockDeliveryWarehouse, IsNull,
Or<CarrierExt.usrHWDELockDeliveryWarehouse, Equal<False>,
Or<SOOrderTypeExt.usrHWDEEnableDeliveryRestrictions, IsNull,
Or<SOOrderTypeExt.usrHWDEEnableDeliveryRestrictions, Equal<False>>>>>>>>>>);
PXDimensionSelectorAttribute attr;
_Attributes.Add(attr = new PXDimensionSelectorAttribute(DimensionName, SearchType, typeof(INSite.siteCD),
new Typen]
{
typeof (INSite.siteCD),typeof (INSite.descr)
}));
attr.CacheGlobal = false;
attr.DirtyRead = false;
attr.DescriptionField = typeof(INSite.descr);
_SelAttrIndex = _Attributes.Count - 1;
}
}

You can see that I updated the search type to be based on joining the current SOOrder’s BranchID to get the warehouses, and ShipVia to check to see if it should lock the settings. 

Then in SOOrderEntry, I overrode the attribute.


PXMergeAttributes(Method = MergeMethod.Replace)]
HWDEDeliveryWarehouseSite(DescriptionField = typeof(INSite.descr))]
protected virtual void _(Events.CacheAttached<SOOrder.defaultSiteID> e)
{
}



So now looking at what I did, I looked and the LabourItemID is tied to the PMLaborItemAttribute, and can follow similar logic.

I would duplicate that attribute, call it a custom attribute, and override the constructors select based on your logic:

 



public PMLaborItemAttribute(Type project, Type earningType, Type employeeSearch)
{
this.projectField = project;
this.earningTypeField = earningType;
this.employeeSearch = employeeSearch;
//update the line below with your logic
PXDimensionSelectorAttribute select = new PXDimensionSelectorAttribute(InventoryAttribute.DimensionName, typeof(Search<InventoryItem.inventoryID, Where<InventoryItem.itemType, Equal<INItemTypes.laborItem>, And<Match<Current<AccessInfo.userName>>>>>), typeof(InventoryItem.inventoryCD));

_Attributes.Add(select);
_SelAttrIndex = _Attributes.Count - 1;
}


Hope this helps push you into the right direction.


@Keith Richardson - Is it possible to construct a BQL ‘not starts with’? That’s the logic I was struggling with, same as with the restrictor. Updating the dimension selector will still need BQL, right?


I found this article constructing a like  , and can maybe do a like string equals false or true depending on what’s needed for that field instead of not starts with?

 

https://stackoverflow.com/questions/39706304/bql-in-pxselector-to-filter-by-a-starts-with


The restrictor part is simple.
It doesn’t allow you to save a line with Earning type “RG” and InventoryCD starts with “X”. The problem is the selector popup never shows any Inventory Item where InventoryCD starts with “X”.

public class TimeCardExt : PXGraphExtension<TimeCardMaint>
{
PXMergeAttributes(Method = MergeMethod.Append)]
nPXRestrictor(typeof(Where<
Brackets<
EPTimeCardSummary.earningType.FromCurrent.IsEqual<EPSetup.EarningTypeRG>.And<
Not<InventoryItem.inventoryCD.StartsWith<STARTS_WITH_X>>>>
.Or<EPTimeCardSummary.earningType.FromCurrent.IsNotEqual<EPSetup.EarningTypeRG>>>),
"Inventory Item is not allowed for this Earning Type")]
protected void _(Events.CacheAttached<TimeCardMaint.EPTimeCardSummaryWithInfo.labourItemID> e) {}
}

public class STARTS_WITH_X : BqlType<IBqlString, string>.Constant<STARTS_WITH_X> {
/// <exclude/>
public STARTS_WITH_X() : base("X") { }
}

 


Let me see if I can whip it up on lunch today


@zfebert56 - I’ve run into issues recently on another project where a PXVerify wouldn’t work correctly with IsNotEqual. Maybe that’s a limitation of that particular attribute, but it seems like BQL should work anywhere BQL is permitted.


I have been going at this, and cannot figure out why the control does not refresh, even though the fieldupdated fires, and validates/invalidates the line.

I made sure CommitChanges=true for earningtype/laboritem and autorefresh=true for labor item were set. This is the same as how I had the other items being filtered.

Then I set my default labor code to XCONSULTPM. This should invalidate when the earning type RG is selected.

So when a new row is inserted, its correct, and does try to add XCONSULTPM to the row, but with a red X, as the default earnings code is RG. It does not show XCONSULTPM in the list.  So I changed it to VL, and it then does not error, but the selector does not change at all.

I checked SQL through request profiler, and it is calling the proper SQL - and I was executing it in SSMS and it has been returning the suggested results.

Maybe this needs to be escalated to support? Not sure what is going on….

FYI here is my customization code.

using PX.Data;
using PX.Data.BQL;
using PX.Data.BQL.Fluent;
using PX.Objects.CS;
using PX.Objects.EP;
using PX.Objects.IN;
using PX.Objects.PM;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static PX.Objects.EP.TimeCardMaint;

namespace HaunWelding
{

public class TimeCardExt : PXGraphExtension<TimeCardMaint>
{
PXMergeAttributes(Method = MergeMethod.Replace)]
CustomPMLaborItem(typeof(EPTimeCardSummary.projectID), typeof(EPTimeCardSummary.earningType), typeof(Select<EPEmployee, Where<EPEmployee.bAccountID, Equal<Current<EPTimeCard.employeeID>>>>))]
protected void _(Events.CacheAttached<EPTimeCardSummaryWithInfo.labourItemID> e) { }


public virtual void _(Events.RowUpdated<EPTimeCardSummary> e, PXRowUpdated del)
{
//added to test breakpoints. It does not hit after updating earningtype.
del?.Invoke(e.Cache, e.Args);
}
public virtual void _(Events.RowUpdated<EPTimeCardSummaryWithInfo> e, PXRowUpdated del)
{
//added to test breakpoints. It does hit after updating earningtype.
del?.Invoke(e.Cache, e.Args);
}


public virtual void _(Events.FieldUpdated<EPTimeCardSummaryWithInfo, EPTimeCardSummaryWithInfo.earningType> e, PXFieldUpdated del)
{
//added to test breakpoints. It does hit after updating earningtype.
del?.Invoke(e.Cache, e.Args);
}

}

public class STARTS_WITH_X : BqlType<IBqlString, string>.Constant<STARTS_WITH_X>
{
/// <exclude/>
public STARTS_WITH_X() : base("X") { }
}


PXDBInt]
PXUIField(DisplayName = "Labor Item")]
public class CustomPMLaborItemAttribute : PXEntityAttribute, IPXFieldDefaultingSubscriber
{
protected Type projectField;
protected Type earningTypeField;
protected Type employeeSearch;
private class earningTypePh : BqlPlaceholderBase { }


public CustomPMLaborItemAttribute(Type project, Type earningType, Type employeeSearch)
{
this.projectField = project;
this.earningTypeField = earningType;
this.employeeSearch = employeeSearch;
//use this one for the default behavior
//Type SearchType = BqlTemplate.OfCommand<Search<InventoryItem.inventoryID,
// Where2<Where<InventoryItem.itemType, Equal<INItemTypes.laborItem>,
// And<Match<Current<AccessInfo.userName>>>>
// , And<Where2<
// Where<Current<earningTypePh>, Equal<EPSetup.EarningTypeRG>, And<Not<InventoryItem.inventoryCD, StartsWith<STARTS_WITH_X>>>>
// , Or<Current<earningTypePh>, NotEqual<EPSetup.EarningTypeRG>>
// >>>>>.Replace<earningTypePh>(earningType)
// .ToType();

//this is to test with X or without X.
Type SearchType = typeof(Search<InventoryItem.inventoryID,
Where2<Where<InventoryItem.itemType, Equal<INItemTypes.laborItem>,
And<Match<Current<AccessInfo.userName>>>>
, And<Where2<
Where<Current<EPTimeCardSummaryWithInfo.earningType>, Equal<EPSetup.EarningTypeRG>, And<Not<InventoryItem.inventoryCD, StartsWith<STARTS_WITH_X>>>>
, Or<Where<Current<EPTimeCardSummaryWithInfo.earningType>, NotEqual<EPSetup.EarningTypeRG>, And<InventoryItem.inventoryCD, StartsWith<STARTS_WITH_X>>>>
>>>>);
PXDimensionSelectorAttribute select = new PXDimensionSelectorAttribute(InventoryAttribute.DimensionName, SearchType
,

typeof(InventoryItem.inventoryCD));
select.CacheGlobal = false;
select.DirtyRead = false;
_Attributes.Add(select);
_SelAttrIndex = _Attributes.Count - 1;
}

public virtual void FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
EPEmployee employee = null;

if (employeeSearch != null)
{
BqlCommand cmd = BqlCommand.CreateInstance(employeeSearch);
PXView view = new PXView(sender.Graph, false, cmd);

employee = view.SelectSingle() as EPEmployee;
}

if (employee != null)
{
int? projectID = (int?)sender.GetValue(e.Row, projectField.Name);
string earningType = (string)sender.GetValue(e.Row, earningTypeField.Name);
int? laborItem = (int?)sender.GetValue(e.Row, FieldName);

if (sender.Graph.IsImportFromExcel && laborItem != null)
{
e.NewValue = laborItem;
}
else
{
e.NewValue = GetDefaultLaborItem(sender.Graph, employee, earningType, projectID);
}
}
}

public virtual int? GetDefaultLaborItem(PXGraph graph, EPEmployee employee, string earningType, int? projectID)
{
if (employee == null)
return null;

int? result = null;

if (ProjectDefaultAttribute.IsProject(graph, projectID))
{
result = EPContractRate.GetProjectLaborClassID(graph, projectID.Value, employee.BAccountID.Value, earningType);
}

if (result == null)
{
result = EPEmployeeClassLaborMatrix.GetLaborClassID(graph, employee.BAccountID, earningType);
}

if (result == null)
{
result = employee.LabourItemID;
}

return result;
}
}
}


 


@Keith Richardson I feel your pain, I wasn’t not able to figure it out either why the control didn’t refresh. I also tried to replace Dimension Selector with a Regular one, and it didn’t make any difference at all.

I hope someone could solve this one, because I really want to know what little piece we all missed.


I feel so validated 😎


Thank you so much @Keith Richardson@darylbowman@Zoltan Febert for you time and effort.


Reply