How to attach an event to the Residential Delivery checkbox on the Customers screen in 2020R2? - acumatica

I need to change the default value of the Residential Delivery checkbox on the Customers AR.30.30.00 screen (Shipping tab) to checked by default. See screenshot:
In 2017R2, this event handler worked without error:
public class CustomerMaint_Extension : PXGraphExtension<CustomerMaint>
{
protected virtual void LocationExtAddress_CResedential_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
var row = (LocationExtAddress)e.Row;
if (row != null)
{
e.NewValue = true; // checked by default
}
}
}
I'm updating this customization for 2020R2. It appears that LocationExtAddress has been replaced with DefLocationExt in newer versions. (Resedential is mis-spelled intentionally in the code... that's how Acumatica defined it.) I've tried changing the event handler to:
protected virtual void DefLocationExt_CResedential_FieldDefaulting(PXCache sender, PXFieldDefaultingEventArgs e)
{
var row = (DefLocationExt)e.Row;
if (row != null)
{
e.NewValue = true; // checked by default
}
}
But this results in a run-time error:
Failed to subscribe the event PX.Objects.AR.CustomerMaint_Extension::DefLocationExt_CResedential_FieldDefaulting in the graph PX.Objects.AR.CustomerMaint. The method signature looks like an event handler, but the cache DefLocationExt has not been found in the list of auto-initialized caches. Remove unused event handlers from the code.
How can I attach an event to this field in 2020R2?

Try a generic event handler and see if you get the same result.
It might look something like this.
protected virtual void _(Events.FieldDefaulting<PX.Objects.CR.Standalone.Location.cResedential> e)
{
var row = (PX.Objects.CR.Standalone.Location)e.Row;
if (row != null)
{
e.NewValue = true; // checked by default
}
}

Related

Running a long operation within an event handler

I need to run some address validation on Customer Location addresses using a 3rd party API to determine if the address is residential or commercial. This validation should run whenever an address field is changed. In other words, the validation should be run in the Address_RowUpdated event handler.
Because the function is calling a 3rd party API, I believe that it should be done in a separate thread, using PXLongOperation so that it does not hold up address saving and fails gracefully if the API is unavailable or returns an error.
However, I am not sure if the architecture of running a long operation within an event handler is supported or if a different approach would be better.
Here is my code.
public class CustomerLocationMaint_Extension : PXGraphExtension<CustomerLocationMaint>
{
protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
if (row != null)
{
Location location = this.Base.Location.Current;
PXCache locationCache = Base.LocationCurrent.Cache;
PXLongOperation.StartOperation(Base, delegate
{
RunCheckResidential(location, locationCache);
});
this.Base.LocationCurrent.Cache.IsDirty = true;
}
}
protected void RunCheckResidential(Location location, PXCache locationCache)
{
string messages = "";
PX.Objects.CR.Address defAddress = PXSelect<PX.Objects.CR.Address,
Where<PX.Objects.CR.Address.addressID, Equal<Required<Location.defAddressID>>>>.Select(Base, location.DefAddressID);
FValidator validator = new FValidator();
AddressValidationReply reply = validator.Validate(defAddress);
AddressValidationResult result = reply.AddressResults[0];
bool isResidential = location.CResedential ?? false;
if (result.Classification == FClassificationType.RESIDENTIAL)
{
isResidential = true;
} else if (result.Classification == FClassificationType.BUSINESS)
{
isResidential = false;
} else
{
messages += "Residential classification is: " + result.Classification + "\r\n";
}
location.CResedential = isResidential;
locationCache.Update(location);
Base.LocationCurrent.Update(location);
Base.Actions.PressSave();
// Display relevant messages
if (reply.HighestSeverity == NotificationSeverityType.SUCCESS)
String addressCorrection = validator.AddressCompare(result.EffectiveAddress, defAddress);
if (!string.IsNullOrEmpty(addressCorrection))
messages += addressCorrection;
}
PXSetPropertyException message = new PXSetPropertyException(messages, PXErrorLevel.Warning);
PXLongOperation.SetCustomInfo(new LocationMessageDisplay(message));
//throw new PXOperationCompletedException(messages); // Shows message if you hover over the success checkmark, but you have to hover to see it so not ideal
}
public class LocationMessageDisplay : IPXCustomInfo
{
public void Complete(PXLongRunStatus status, PXGraph graph)
{
if (status == PXLongRunStatus.Completed && graph is CustomerLocationMaint)
{
((CustomerLocationMaint)graph).RowSelected.AddHandler<Location>((sender, e) =>
{
Location location = e.Row as Location;
if (location != null)
{
sender.RaiseExceptionHandling<Location.cResedential>(location, location.CResedential, _message);
}
});
}
}
private PXSetPropertyException _message;
public LocationMessageDisplay(PXSetPropertyException message)
{
_message = message;
}
}
}
UPDATE - New Approach
As suggested, this code now calls the LongOperation within the Persist method.
protected virtual void Address_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
PX.Objects.CR.Address row = (PX.Objects.CR.Address)e.Row;
if (row != null)
{
Location location = Base.Location.Current;
LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);
locationExt.UsrResidentialValidated = false;
Base.LocationCurrent.Cache.IsDirty = true;
}
}
public delegate void PersistDelegate();
[PXOverride]
public virtual void Persist(PersistDelegate baseMethod)
{
baseMethod();
var location = Base.Location.Current;
PXCache locationCache = Base.LocationCurrent.Cache;
LocationExt locationExt = PXCache<Location>.GetExtension<LocationExt>(location);
if (locationExt.UsrResidentialValidated == false)
{
PXLongOperation.StartOperation(Base, delegate
{
CheckResidential(location);
});
}
}
public void CheckResidential(Location location)
{
CustomerLocationMaint graph = PXGraph.CreateInstance<CustomerLocationMaint>();
graph.Clear();
graph.Location.Current = location;
LocationExt locationExt = location.GetExtension<LocationExt>();
locationExt.UsrResidentialValidated = true;
try
{
// Residential code using API (this will change the value of the location.CResedential field)
} catch (Exception e)
{
throw new PXOperationCompletedWithErrorException(e.Message);
}
graph.Location.Update(location);
graph.Persist();
}
PXLongOperation is meant to be used in the context of a PXAction callback. This is typically initiated by a menu item or button control, including built-in actions like Save.
It is an anti-pattern to use it anytime a value changes in the web page. It should be used only when a value is persisted (by Save action) or by another PXAction event handler. You should handle long running validation when user clicks on a button or menu item not when he changes the value.
For example, the built in Validate Address feature is run only when the user clicks on the Validate Address button and if validated requests are required it is also run in a Persist event called in the context of the Save action to cancel saving if validation fails.
This is done to ensure user expectation that a simple change in a form/grid value field doesn't incur a long validation wait time that would lead the user to believe the web page is unresponsive. When the user clicks on Save or a specific Action button it is deemed more reasonable to expect a longer wait time.
That being said, it is not recommended but possible to wrap your PXLongOperation call in a dummy Action and asynchronously click on the invisible Action button to get the long operation running in the proper context from any event handler (except Initialize):
using PX.Data;
using System.Collections;
namespace PX.Objects.SO
{
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> TestLongOperation;
[PXUIField(DisplayName = "Test Long Operation", Visible = false, Visibility = PXUIVisibility.Invisible)]
[PXButton]
public virtual IEnumerable testLongOperation(PXAdapter adapter)
{
PXLongOperation.StartOperation(Base, delegate ()
{
System.Threading.Thread.Sleep(2000);
Base.Document.Ask("Operation Done", MessageButtons.OK);
});
return adapter.Get();
}
public void SOOrder_OrderDesc_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
{
if (!PXLongOperation.Exists(Base.UID))
{
// Calling Action Button asynchronously so it can run in the context of a PXAction callback
Base.Actions["TestLongOperation"].PressButton();
}
}
}
}

Enable SOLine field after Order Completed

I need to enable the Salesperson ID and Commissionable fields of Sales Order Lines for Sales Orders in the Completed state.
I referenced the question here about enabling fields in the SOOrder header: How to enable CustomerOrderNbr field in Sales Order screen?
I added the two fields to the Automation Steps for the SO Complete step
And added customization code:
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
public void SOOrderLine_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
SOOrderLine line = e.Row as SOOrderLine;
if (line == null) return;
PXUIFieldAttribute.SetEnabled<SOOrderLine.salesPersonID>(sender, line, true);
PXUIFieldAttribute.SetEnabled<SOOrderLine.commissionable>(sender, line, true);
}
}
However, the fields are still disabled. Is there something I'm missing?
I have a similar requirement with one of my clients. You're on the right track with automation steps, but you need something else to enable editing. Here are the two event handlers we use:
protected void SOOrder_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
// Make the promised on ship date field editable even after the order has been completed.
// This code is not enough to make the feature work - automation steps need to be modified for SO Completed and SO Invoiced to ensure the
// caches are not disabled.
sender.AllowUpdate = true;
Base.Transactions.Cache.AllowUpdate = true;
}
protected void SOLine_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
if (Base.Document.Current != null)
{
//Automation steps were modified to keep the transactions grid enabled for the completed status; we are manually disabling it here but leaving the promised on ship date field editable.
if(Base.Document.Current.Status == SOOrderStatus.Completed)
PXUIFieldAttribute.SetEnabled(sender, e.Row, false);
PXUIFieldAttribute.SetEnabled<SOLineExt.usrPromisedShipOnDate>(sender, e.Row, true);
PXUIFieldAttribute.SetEnabled<SOLineExt.usrLateReasonCode>(sender, e.Row, true);
}
}
To finish out the solution to this, in this case I found it was not necessary to Enable the full Sales Order Line via Automation Steps and then disable it via SOLine_RowSelect. It was, however, necessary to add the Sales Order > Order Nbr field to the automation steps (to make the document Save available after changing the Sales Order line). And strangely it was also necessary for us to give this Customization Project a higher Level than the others implementing it after other customizations that may have made changes to the same screen or objects.
public class SOOrderEntry_Extension : PXGraphExtension<SOOrderEntry>
{
protected void SOOrder_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
sender.AllowUpdate = true;
Base.Transactions.Cache.AllowUpdate = true;
}
protected void SOLine_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
if (Base.Document.Current != null)
{
PXUIFieldAttribute.SetEnabled<SOLine.salesPersonID>(sender, e.Row, true);
PXUIFieldAttribute.SetEnabled<SOLine.commissionable>(sender, e.Row, true);
}
}
}

Show popup or smart dialog once on screen load

I want to show a popup with some message to user whenever screen gets loaded for the first time for each record.
For example when Sales Order screen is loaded for particular sales order, it should show the popup only once. Then user navigates to a next sales order, it should again show the popup for that particular sales order only once.
I have written the code in constructor and RowSelected event, but it does not has the Current record. That is CRCurrentCaseNotes is always null in both this events. However, with the button (ViewNotes in below code sample), it works.
[PXViewName("CRCurrentCaseNotes")]
[PXCopyPasteHiddenView]
public PXSelect<Note,
Where<CRCase.caseID, Equal<Current<CRCase.caseID>>>> CRCurrentCaseNotes;
public CRCaseMaintExtension()
: base()
{
if (CRCurrentCaseNotes.Current != null)
{
CRCurrentCaseNotes.AskExt();
}
}
protected virtual void CRCase_RowSelecting(PXCache cache, PXRowSelectingEventArgs e)
{
var caseRow = (CRCase)e.Row;
if (caseRow == null) return;
if (CRCurrentCaseNotes.Current != null)
{
CRCurrentCaseNotes.AskExt();
}
}
// SAME CODE WORKS WITH THE BUTTON CLICK
public PXAction<CRCase> viewNotes;
[PXUIField(DisplayName = "View Notes")]
[PXButton]
protected virtual IEnumerable ViewNotes(PXAdapter adapter)
{
if (CRCurrentCaseNotes.Current != null)
{
CRCurrentCaseNotes.AskExt();
}
return adapter.Get();
}
Try to show the dialog when the primary DAC key field is modified:
protected void CRCase_CaseID_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
{
CRCase case = e.Row as CRCase;
if (case != null && case.CaseID != null && !e.ExternalCall)
{
// Show dialog
}
}
If there's no other way you could use JavaScript to show/hide the SmartPanel:
document.getElementById("<%=SmartPanelID.ClientID %>").style.display = 'block';
EDIT:
Dialog can only be shown from an action event handler (FieldUpdated won't work) or from JavaScript. To open dialog when the page open, you can try hooking DocumentReady event in JavaScript and call the Acumatica action from JavaScript too: px_alls['ds'].executeCallback('ActionName');

Disabling fields on Bills and Adjustments doesn't seem to work

I have a custom dropdown field on Bills and Adjustments which I want to determine when to disable specific fields on the screen. I'm using the following logic, which doesn't seem to work (the commented lines didn't work either). I've set the commitchanges to true on the user field - and I've stepped through the code to make sure it's getting hit:
protected virtual void APInvoice_UsrPOStatus_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e)
{
var apr = (APRegister)e.Row;
if (apr == null) return;
var aprext = PXCache<APRegister>.GetExtension<APRegisterExt>(apr);
if (aprext == null) return;
if (aprext.UsrPOstatus != "Open")
{
PXUIFieldAttribute.SetEnabled<APRegister.docType>(sender, apr, false);
PXUIFieldAttribute.SetEnabled<APRegister.refNbr>(sender, apr, false);
//PXUIFieldAttribute.SetEnabled<APInvoice.docType>(Base.Document.Cache, null, false); //(OpenSourceDataDetail.Cache, null, true);
//PXUIFieldAttribute.SetEnabled<APInvoice.refNbr>(Base.Document.Cache, null, false);
}
}
I get no errors, but nothing happens. Is it not possible to disable these fields?
I'm also not sure whether to use APInvoice or APRegister for these statements.
When a field enabled state and visibility can't be changed it's usually because a later event overrides your changes.
The base graph of the graph extension you're working on (APInvoiceEntry) calls SetEnabled on these fields in the APInvoice_RowSelected event.
To override these calls you should override the same event in your extension, then your event handler will be the last one executed.
protected virtual void APInvoice_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
  APInvoice apInvoice = e.Row as APInvoice;
  if (apInvoice == null)
  {
    return;
  }
  PXUIFieldAttribute.SetEnabled<APInvoice.docType>(cache, apInvoice, false);
  PXUIFieldAttribute.SetEnabled<APInvoice.refNbr>(cache, apInvoice, false);
}

Make Salesperson ID a Required field on SOLine

I need to make Salesperson ID on SOLine as a required field. But as Transfer orders do not have Salesperson, hence it should only validate when I create orders other than Transfer orders.
I tried with below code but it seems it is not working. Might be it is overrided with some existing code. Let me know if anyone has any suggestions.
public PXSetup<SOOrderTypeOperation,
Where<SOOrderTypeOperation.orderType, Equal<Optional<SOOrderType.orderType>>,
And<SOOrderTypeOperation.operation, Equal<Optional<SOOrderType.defaultOperation>>>>> sooperation;
protected bool IsTransferOrder
{
get
{
return (sooperation.Current.INDocType == INTranType.Transfer);
}
}
protected virtual void SOLine_RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
{
var row = (SOLine)e.Row;
if (row == null) return;
PXDefaultAttribute.SetPersistingCheck<SOLine.salesPersonID>(sender, row, IsTransferOrder ? PXPersistingCheck.Nothing : PXPersistingCheck.Null);
}
I usually thrown an appropriate exception in Row Persisting when the condition exists.
Here is an example from SOShipmentEntry checking for transfer and checking the null value of a field:
protected virtual void SOShipment_RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
{
SOShipment doc = (SOShipment)e.Row;
if (doc.ShipmentType == SOShipmentType.Transfer && doc.DestinationSiteID == null)
{
throw new PXRowPersistingException(typeof(SOOrder.destinationSiteID).Name, null, ErrorMessages.FieldIsEmpty, typeof(SOOrder.destinationSiteID).Name);
}
}
I have also called RaiseExceptionHandling similar to this example within RowPersisting
// sender = PXCache
if (row.OrderQty == Decimal.Zero)
sender.RaiseExceptionHandling<POLine.orderQty>(row, row.OrderQty, new PXSetPropertyException(Messages.POLineQuantityMustBeGreaterThanZero, PXErrorLevel.Error));
Both examples should stop the page from the save. calling the Raise Exception handling should point out the field with the Red X which is the better approach and easier for the user to find the field in question.
For your example:
protected virtual void SOLine_RowPersisting(PXCache sender, PXRowPersistingEventArgs e)
{
SOLine row = (SOLine)e.Row;
if (row == null)
{
return;
}
if (!IsTransferOrder && row.SalesPersonID == null)
{
sender.RaiseExceptionHandling<SOLine.salesPersonID>(row, row.SalesPersonID, new PXSetPropertyException(ErrorMessages.FieldIsEmpty, PXErrorLevel.Error));
}
}

Resources