I am attempting to insert the Customer ID at the beginning of the Project Description. I believe the two opportunities to do this are 1) upon selection of a customer and 2) upon selection of template if the customer has already been selected. I also believe that I want to extend the PM.ProjectEntry business logic. I am having two issues:
Using the same code from the source code in the extension is giving me an error when I try to select the customer record on the CustomerID field updated event. The error is identified in the code below.
I have an TemplateID field updated event and what I thought was an extension of the DefaultFromTemplateProjectSettings event. No errors but the description field does not get modified.
namespace PX.Objects.PM
{
public class ProjectEntry_Extension : PXGraphExtension<PX.Objects.PM.ProjectEntry>
{
#region Event Handlers
// Attempt to modify the project description line after a customer has been selected
protected virtual void _(Events.FieldUpdated<PMProject, PMProject.customerID> e, PXFieldUpdated baseMethod)
{
baseMethod(e.Cache, e.Args);
if (e.Row != null)
{
// Line 3 of this select statement errors with:
// Argument 1: cannot convert from 'PX.Objects.PM.ProjectEntry_Extension' to 'PX.Data.PXGraph'
Customer customer = new PXSelect<Customer,
Where<Customer.bAccountID,
Equal<Required<Customer.bAccountID>>>>(this).Select(e.Row.CustomerID);
if (customer != null)
{
e.Cache.SetValueExt<PMProject.description>(e.Row, customer.AcctCD + " " + e.Row.Description);
}
}
}
// Two attempts to modify the project descrition after the project template has been selected
// Neither of these two codes error, they just don't update the project description field
// "customer.AcctCD" would be determined similar to the above process that currently errors
protected virtual void DefaultFromTemplateProjectSettings(PMProject prj, PMProject templ)
{
prj.Description = "customer.AcctCD" + ": " + templ.Description;
}
protected virtual void _(Events.FieldUpdated<PMProject, PMProject.templateID> e, PXFieldUpdated baseMethod)
{
baseMethod(e.Cache, e.Args);
if (e.Row != null)
{
e.Cache.SetValueExt<PMProject.description>(e.Row, "customer.AcctCD" + ": " + e.Row.Description);
}
}
#endregion
}
}
In the PM.ProjectEntry business logic, I found the following:
protected virtual void OnDefaultFromTemplateTasksInserted(ProjectEntry target, Dictionary<int, int> taskMap)
{
//this method is used to extend DefaultFromTemplate in Customizations.
}
So, I tried the following in my customization. There are no errors but it doesn't alter the project description when the template is selected.
protected virtual void OnDefaultFromTemplateTasksInserted(PMProject prj, PMProject templ, Dictionary<int, int> taskMap)
{
prj.Description = "CustomerID: " + templ.Description;
}
Without the "CustomerID: ", that is the line of code used in the DefaultFromTemplateProjectSettings object.
Regarding the first error, you simply need to change the line
Customer customer = new PXSelect<Customer,
Where<Customer.bAccountID,
Equal<Required<Customer.bAccountID>>>>(this).Select(e.Row.CustomerID);
to
Customer customer = new PXSelect<Customer,
Where<Customer.bAccountID,
Equal<Required<Customer.bAccountID>>>>(this.Base).Select(e.Row.CustomerID);
You need to pass the PXGraph instance to the PXSelect constructor and you currently are passing this which is PXGraphExtension.
Regarding the second part, I don't think anonymous event handlers have the concept of the BaseMethod, so I would try to remove the second parameter of that function.
With a little help I have a solution.
namespace PX.Objects.PM
{
public class ProjectEntry_Extension : PXGraphExtension<PX.Objects.PM.ProjectEntry>
{
#region Event Handlers
// Attempt to modify the project description line after a customer has been selected
protected virtual void _(Events.FieldUpdated<PMProject, PMProject.customerID> e, PXFieldUpdated baseMethod)
{
baseMethod(e.Cache, e.Args);
if (e.Row != null) // attempted to prevent repatition -- && e.Row.Description.IndexOf(":") > 0
{
Customer customer = new PXSelect<Customer,
Where<Customer.bAccountID,
Equal<Required<Customer.bAccountID>>>>(this.Base).Select(e.Row.CustomerID);
if (customer != null)
{
e.Cache.SetValueExt<PMProject.description>(e.Row, customer.AcctCD + ": " + e.Row.Description);
}
}
}
// Attempt to modify the project description line after a template has been selected
public delegate void DefaultFromTemplateDelegate(PMProject prj, int? templateID, ProjectEntry.DefaultFromTemplateSettings settings);
[PXOverride]
public void DefaultFromTemplate(PMProject prj, int? templateID, ProjectEntry.DefaultFromTemplateSettings settings, DefaultFromTemplateDelegate baseMethod)
{
baseMethod(prj, templateID, settings);
if (Base.Project.Current.CustomerID != null)
{
Customer customer = new PXSelect<Customer,
Where<Customer.bAccountID,
Equal<Required<Customer.bAccountID>>>>(this.Base).Select(prj.CustomerID);
if (customer != null)
{
Base.Project.Current.Description = customer?.AcctCD + ": " + Base.Project.Current.Description;
}
}
}
#endregion
}
}
Related
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);
}
I need help to update the value of Project's attribute from different page.
I have fetched the attribute value in 'Appointments' page using following code.
protected void FSAppointment_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected InvokeBaseHandler)
{
if (InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (FSAppointment)e.Row;
AppointmentEntry graph = (AppointmentEntry)cache.Graph;
if (graph.ServiceOrderRelated.Current != null)
{
int? projectID = graph.ServiceOrderRelated.Current.ProjectID;
ProjectEntry projectGraph = PXGraph.CreateInstance<ProjectEntry>();
projectGraph.Project.Current = projectGraph.Project.Search<PMProject.contractID>(projectID);
foreach (CSAnswers att in projectGraph.Answers.Select())
{
if (att.AttributeID == "ESTHOURS")
{
cache.SetValueExt<FieldService.ServiceDispatch.FSAppointmentExt.usrProjectEstimatedRemainingHours>(row, att.Value);
return;
}
}
}
}
And, now I want user to be able to update that particular attribute's value from 'Appointments' page.
For that, I had written following code by overriding the Persist method of 'Appointments' page.
public delegate void PersistDelegate();
[PXOverride]
public void Persist(PersistDelegate baseMethod)
{
if (Base.ServiceOrderRelated.Current != null)
{
using (PXTransactionScope scope = new PXTransactionScope())
{
int? projectID = Base.ServiceOrderRelated.Current.ProjectID;
ProjectEntry projectGraph = PXGraph.CreateInstance<ProjectEntry>();
var project = projectGraph.Project.Search<PMProject.contractID>(projectID);
var answers = projectGraph.Answers.Select();
foreach (CSAnswers att in answers)
{
if (att.AttributeID == "ESTHOURS")
{
att.Value = "20";
}
}
projectGraph.Actions.PressSave();
}
}
baseMethod();
}
But still it is not updating the value.
The thing to realize is that the attribute system is different that the usual DAC->SQL system. CSAnswers is a unnormalized table of values for all the attributes. They are linked to a DAC document by RefNoteID. See select * from CSAnswers where AttributeID = 'ESTHOURS' In the code above you are altering every project's 'esthours'. You're also missing a statement where you tell the graph to update the cache with your altered object. Something like projectGraph.Answers.Update(att);
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));
}
}