I have a customization to the Sales Orders screen, where I use a RowPersisting event to update the Requested Date to the current date upon saving. The problem is, if the save is not executed, but the Actions.PrepareInvoice is initiated, the new Requested Date (hopefully set by the RowPersisting event) is not used. I've tried to override the base method as follows (to save the Sales Order record with the new Requested Date before the Prepare Invoice process is run):
public delegate IEnumerable PrepareInvoiceDelegate(PXAdapter adapter);
[PXOverride]
public IEnumerable PrepareInvoice(PXAdapter adapter, PrepareInvoiceDelegate baseMethod)
{
Base.Actions.PressSave();
return baseMethod(adapter);
}
But I receive the following error - "Error: The previous operation has not been complete yet."
How can I ensure that a modified Requested Date is used for the Prepare Invoice process if the Sales Order record is not yet saved?
The PrepareInvoice method in case if the call is not from Processing Page(adapter.MassProcess==false) is calling this.Save.Press() anyway.
Below is the code from PrepareInvoice action. Before creation of the Invoice there is being called this.Save.Press() so any your update will be saved and used for creation of the invoice.
[PXUIField(DisplayName = "Prepare Invoice", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select, Visible = false), PXButton]
public virtual IEnumerable PrepareInvoice(PXAdapter adapter)
{
List<SOOrder> list = adapter.Get<SOOrder>().ToList<SOOrder>();
foreach (SOOrder current in list)
{
if (this.Document.Cache.GetStatus(current) != PXEntryStatus.Inserted)
{
this.Document.Cache.SetStatus(current, PXEntryStatus.Updated);
}
}
if (!adapter.MassProcess)
{
try
{
this.RecalculateAvalaraTaxesSync = true;
this.Save.Press();
}
finally
{
this.RecalculateAvalaraTaxesSync = false;
}
}
PXLongOperation.StartOperation(this, delegate
{
DocumentList<ARInvoice, SOInvoice> documentList = new DocumentList<ARInvoice, SOInvoice>(PXGraph.CreateInstance<SOShipmentEntry>());
SOOrderEntry.InvoiceOrder(adapter.Arguments, list, documentList, adapter.MassProcess);
if (!adapter.MassProcess && documentList.Count > 0)
{
using (new PXTimeStampScope(null))
{
SOInvoiceEntry sOInvoiceEntry = PXGraph.CreateInstance<SOInvoiceEntry>();
sOInvoiceEntry.Document.Current = sOInvoiceEntry.Document.Search<ARInvoice.docType, ARInvoice.refNbr>(documentList[0].DocType, documentList[0].RefNbr, new object[]
{
documentList[0].DocType
});
throw new PXRedirectRequiredException(sOInvoiceEntry, "Invoice");
}
}
});
return list;
}
The solution seems to be to recreate the Action method in a graph extension and add the following code:
if (!adapter.MassProcess)
{
//****Code added to update the Requested Date to today's date...
var soorder = (SOOrder)Base.Caches[typeof(SOOrder)].Current;
soorder.RequestDate = DateTime.Now;
Base.Caches[typeof(SOOrder)].Update(soorder);
//****End of code added...
try
{
Base.RecalculateAvalaraTaxesSync = true;
Base.Save.Press();
}
finally
{
Base.RecalculateAvalaraTaxesSync = false;
}
}
Related
I'm trying to store results from a BQL statement in a list by looping through and adding the objects to the list during a PXAction, so I can run some checks against the list later in a FieldUpdated event. However When I get to the FieldUpdated event, the contents of the list are back to zero.
public class SOShipmentEntry_Extension : PXGraphExtension<SOShipmentEntry>
{
public PXSelectJoin<INItemLotSerial, InnerJoin<SOShipLine, On<INItemLotSerial.inventoryID, Equal<Current<SOShipLine.inventoryID>>>>, Where<SOShipLine.shipmentNbr, Equal<Current<SOShipment.shipmentNbr>>>> AvailSerialList;
List<INItemLotSerial> AvailToPick = new List<INItemLotSerial>();
Here's where I add results into the list (added a watch on debugger to make sure I have a count of the content added, and verify that the list has content)
public PXAction<SOShipment> uploadlotserial;
[PXUIField(DisplayName = "Upload LotSerial", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)]
[PXButton]
public virtual IEnumerable uploadLotserial(PXAdapter adapter)
{
if (AvailToPick.Count == 0)
{
foreach (INItemLotSerial pick in AvailSerialList.Select(this))
{
AvailToPick.Add(pick);
}
}
if (SerialFilter.AskExt() == WebDialogResult.OK)
{
GenerateSoShipmentSplitLine();
DeleteAllTempRecords();
}
DeleteAllTempRecords();
SerialList.Cache.Clear();
SerialList.Cache.ClearQueryCache();
return adapter.Get();
}
However when I hit this event, the count shows zero.
protected void InfoLotSerialFilter_LotSerialNbr_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if (InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (InfoLotSerialFilter)e.Row;
if (row.LotSerialNbr != null)
{
if (AvailToPick.Count > 0)
{
Any insight as to why the list contents are cleared would be appreciated. Thank you!
I would suggest that instead of the list you create a simple View for INItemLotSerial that will create a cache when the page is run. You can Insert records into the cache during your action and then push them into a List<> and count them during your event.
we have requirement to duplicate the confirm shipment action button for some business work and also need to update some custom fields on confirm shipment long run operation is completed.
Below is my code but while doing cache update i am getting Error: Collection was modified; enumeration operation may not execute.
Please correct me where i am doing wrong
public PXAction<PX.Objects.SO.SOShipment> ConfirmShipment;
[PXUIField(DisplayName = "Confirm Shipment")]
[PXButton]
protected virtual IEnumerable confirmShipment(PXAdapter adapter)
{
if (ShipFilter.Current != null)
{
var soOrderShip = Base.Document.Current;
if (soOrderShip != null)
{
var graph = PXGraph.CreateInstance<SOShipmentEntry>();
//We are recreating an adapter like the framework would do.
var a = new PXAdapter(graph.Document)
{
Searches = new object[] { soOrderShip.ShipmentNbr }
};
using (PXTransactionScope ts = new PXTransactionScope())
{
//Note: Confirm Shipment is Action 1 :
a.Arguments.Add("actionID", 1);
PXLongOperation.StartOperation(Base, () => { foreach (SOShipment soShipment in graph.action.Press(a)) ; });
//PXLongOperation.WaitCompletion(graph.UID);
PXAutomation.CompleteAction(graph);
PXLongOperation.WaitCompletion(graph.UID);
PXLongOperation.ClearStatus(graph.UID);
graph.Document.Cache.SetValueExt<SOShipmentExt.usrKWMXDCTimeStamp>(soOrderShip, Convert.ToDateTime(Convert.ToDateTime(new PX.Data.PXGraph().Accessinfo.BusinessDate).ToShortDateString() + " " + PX.Common.PXTimeZoneInfo.Now.ToLongTimeString()));
graph.Document.Cache.SetValueExt<SOShipmentExt.usrKWMXPieceCount>(soOrderShip, Convert.ToDecimal(Base.Document.Current.ShipmentQty));
graph.Document.Cache.SetValueExt<SOShipmentExt.usrKWMXEnteredBy>(soOrderShip, this.ShipFilter.Current.EnteredBy);
graph.Document.Update(soOrderShip);
graph.Save.Press();
ts.Complete();
}
}
}
return adapter.Get();
}
Thanks in advance.
You should override the Confirmation routine, execute the Base operation, and then add your code.
Extend existing event
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();
}
}
}
}
I am trying to copy some user fields from the CRM Quote to the Sales Order. The CRM Quote uses a different object than the Sales Quote and there doesn't appear to be a way to relate it back. I tried overriding the Create Sales Order to add a handler, but this didn't seem to work Any help would be appreciated. Here is the code I tried:
public class OpportunityMaint_Extension : PXGraphExtension<OpportunityMaint>
{
public delegate IEnumerable CreateSalesOrderDelegate(PXAdapter adapter);
[PXOverride]
public virtual IEnumerable CreateSalesOrder(PXAdapter adapter, CreateSalesOrderDelegate baseMethod)
{
Base.RowInserting.AddHandler<SOLine>((sender, e) =>
{
SOLine orderLine = e.Row as SOLine;
if (orderLine == null) return;
SOLineExt orderLineExt = orderLine.GetExtension<SOLineExt>();
var product = Base.Products.Current;
CROpportunityProductsExt productExt = product.GetExtension<CROpportunityProductsExt>();
orderLineExt.UsrHasAnticipatedDiscount = productExt.UsrHasAnticipatedDiscount;
orderLineExt.UsrAnticipatedDiscountPct = productExt.UsrAnticipatedDiscountPct;
orderLineExt.UsrAnticipatedDiscountAmt = productExt.UsrAnticipatedDiscountAmt;
orderLineExt.UsrAnticipatedUnitPrice = productExt.UsrAnticipatedUnitPrice;
orderLineExt.UsrTotalAnticipatedDiscountAmt = productExt.UsrTotalAnticipatedDiscountAmt;
});
return baseMethod(adapter);
}
}
Thanks!
There are two posts with answers to this same question:
Populate custom field while creating sale order from opportunity
How to pass custom field vales from Opportunity to sales Order?
To sum it up, you can add a rowinserting event handler within the button action or my preference is within DoCreateSalesOrder (extending OpportunityMaint) like the example below...
[PXOverride]
public virtual void DoCreateSalesOrder(OpportunityMaint.CreateSalesOrderFilter param, Action<OpportunityMaint.CreateSalesOrderFilter> del)
{
PXGraph.InstanceCreated.AddHandler<SOOrderEntry>(graph =>
{
graph.RowInserting.AddHandler<SOLine>((cache, args) =>
{
var soLine = (SOLine)args.Row;
if (soLine == null)
{
return;
}
CROpportunityProducts opProduct = PXResult<CROpportunityProducts>.Current;
if (opProduct == null)
{
return;
}
var opProductExt = PXCache<CROpportunityProducts>.GetExtension<CROpportunityProductsExt>(opProduct);
var soLineExt = PXCache<SOLine>.GetExtension<SOLineExt>(soLine);
//Copy all extension fields here...
});
});
del(param);
}
When I try to add a package to a shipment, even if I put the confirmed value to false, Acumatica seems to overwrite it with the true value.
Here is the code sample I am using for my first call as the second one is only setting the confirmed value to false again.
static void Main(string[] args)
{
DefaultSoapClient client = new DefaultSoapClient();
client.Login("admin", "admin", "Company", null, null);
Shipment ship = new Shipment
{
ShipmentNbr = new StringSearch { Value = "001301", Condition = StringCondition.Equal },
Packages = new ShipmentPackage[]
{
new ShipmentPackage
{
BoxID = new StringValue {Value = "Large" },
Confirmed = new BooleanValue {Value=false },
Weight = new DecimalValue {Value = 1.5m }
}
}
};
client.Put(ship);
client.Logout();
}
The issue here is that there is an event in the SOShipment Graph (SOPackageDetail_Weight_FieldUpdated) which will change the value of the confirmed checkbox to true when the weigh field is updated.
A simple fix for this is to add a small customization, that will disable the content of that event when using the contract based API.
public class SOShipmentEntry_Extension : PXGraphExtension<SOShipmentEntry>
{
#region Event Handlers
protected void SOPackageDetail_Weight_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if (InvokeBaseHandler != null)
if (!Base.IsContractBasedAPI)
InvokeBaseHandler(sender, e);
}
#endregion
}
Though if you do not want this event to happen at any other time you could always add the event but leave it empty.