Base.Actions.PressSave() not working on Attributes with mass actions - acumatica

I created a custom Action on Contracts that updates the description as well as attributes.
It works fine when doing a single entry and pressing the action button but during Mass Action only the description is updated and saved the attributes are not.
What do I need to do to make the Attributes values save correctly during Mass Action?
public PXSelect<CSAnswers,
Where<CSAnswers.refNoteID, Equal<Current<Contract.noteID>>>> CSAttr;
protected IEnumerable testAction(PXAdapter adapter)
{
//get the activation parameters
var a = CSAttr.Select();
Contract mycontract = Base.CurrentContract.Select();
foreach (CSAnswers item in a.ToList())
{
if (item.AttributeID == "ACTA")
{
item.Value = "Won't Update1";
CSAttr.Update(item); //shouldn't this update?
}
else if (item.AttributeID == "ACTB") //desired mode set by user
{
item.Value = "Won't Update2";
CSAttr.Update(item); //shouldn't this update?
}
}
mycontract.Description = "This Works Fine";
Base.CurrentContract.Update(mycontract);
Base.Actions.PressSave();
return adapter.Get();
}

No need to declare custom CSAttr data view: ContractMaint already contains Answers data view to work with Attributes.
TestAction implemented as below, successfully updates both Contract Description and Attribute Value on a brand new 6.00.1596 Acumatica ERP instance when launched from GI set up as entry-point list:
public class ContractMaintExt : PXGraphExtension<ContractMaint>
{
public override void Initialize()
{
TestAction.IsMass = true;
}
public PXAction<Contract> TestAction;
[PXButton]
[PXUIField(DisplayName = "Test Action")]
public void testAction()
{
Contract mycontract = Base.CurrentContract.Select();
foreach (CSAnswers attr in Base.Answers.Select())
{
if (attr.AttributeID == "CONFIGURAB")
{
attr.Value = "Updated";
Base.Answers.Update(attr); //shouldn't this update?
}
}
mycontract.Description = "This Works Fine";
Base.Contracts.Update(mycontract);
Base.Actions.PressSave();
}
}

Related

Overriding PXFilteredProcessingJoin and delegate for APPrintChecks

I have the same issue as the below link but with a different graph (APPrintChecks)
how-do-i-override-pxfilteredprocessingjoin-in-a-graph-extension-without-altering
I am overriding the main view to pull in the remittance name from APContact to show in the grid.
[PXFilterable]
public PXFilteredProcessingJoin<APPayment, PrintChecksFilter,
InnerJoin<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>>>,
Where<boolTrue, Equal<boolTrue>>,
OrderBy<Asc<Vendor.acctName, Asc<APPayment.refNbr>>>> APPaymentList;
However, I do not know how to override the delegate so I won't have the same problem as the other poster (no filter being applied).
protected virtual IEnumerable appaymentlist()
{
if (cleared)
{
foreach (APPayment doc in APPaymentList.Cache.Updated)
{
doc.Passed = false;
}
}
foreach (PXResult<APPayment, Vendor, PaymentMethod, CABatchDetail> doc in PXSelectJoin<APPayment,
InnerJoinSingleTable<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<PaymentMethod, On<PaymentMethod.paymentMethodID, Equal<APPayment.paymentMethodID>>,
LeftJoin<CABatchDetail, On<CABatchDetail.origModule, Equal<BatchModule.moduleAP>,
And<CABatchDetail.origDocType, Equal<APPayment.docType>,
And<CABatchDetail.origRefNbr, Equal<APPayment.refNbr>>>>>>>,
Where2<Where<APPayment.status, Equal<APDocStatus.pendingPrint>,
And<CABatchDetail.batchNbr, IsNull,
And<APPayment.cashAccountID, Equal<Current<PrintChecksFilter.payAccountID>>,
And<APPayment.paymentMethodID, Equal<Current<PrintChecksFilter.payTypeID>>,
And<Match<Vendor, Current<AccessInfo.userName>>>>>>>,
And<APPayment.docType, In3<APDocType.check, APDocType.prepayment, APDocType.quickCheck>>>>.Select(this))
{
yield return new PXResult<APPayment, Vendor>(doc, doc);
if (_copies.ContainsKey((APPayment)doc))
{
_copies.Remove((APPayment)doc);
}
_copies.Add((APPayment)doc, PXCache<APPayment>.CreateCopy(doc));
}
}
There are other private variables that are referenced in this. Any help appreciated.
Also, if there's a simpler way to pull in a related value on a grid like this (virtual field in DAC?) I'm not stuck on doing it with a graph extension.
So this appears to work but it seems messy and duplicates a lot of code and private variables. Appreciate any feedback if there's a better way to do this:
public class APPrintChecks_Extension : PXGraphExtension<APPrintChecks> {
[PXFilterable]
public PXFilteredProcessingJoin<APPayment, PrintChecksFilter,
InnerJoin<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>>>,
Where<boolTrue, Equal<boolTrue>>,
OrderBy<Asc<Vendor.acctName, Asc<APPayment.refNbr>>>> APPaymentList;
public IEnumerable appaymentlist()
{
if (cleared)
{
foreach (APPayment doc in APPaymentList.Cache.Updated)
{
doc.Passed = false;
}
}
foreach (PXResult<APPayment, Vendor, APContact, PaymentMethod, CABatchDetail> doc in PXSelectJoin<APPayment,
InnerJoinSingleTable<Vendor, On<Vendor.bAccountID, Equal<APPayment.vendorID>>,
InnerJoin<APContact, On<APContact.contactID, Equal<APPayment.remitContactID>>,
InnerJoin<PaymentMethod, On<PaymentMethod.paymentMethodID, Equal<APPayment.paymentMethodID>>,
LeftJoin<CABatchDetail, On<CABatchDetail.origModule, Equal<BatchModule.moduleAP>,
And<CABatchDetail.origDocType, Equal<APPayment.docType>,
And<CABatchDetail.origRefNbr, Equal<APPayment.refNbr>>>>>>>>,
Where2<Where<APPayment.status, Equal<APDocStatus.pendingPrint>,
And<CABatchDetail.batchNbr, IsNull,
And<APPayment.cashAccountID, Equal<Current<PrintChecksFilter.payAccountID>>,
And<APPayment.paymentMethodID, Equal<Current<PrintChecksFilter.payTypeID>>,
And<Match<Vendor, Current<AccessInfo.userName>>>>>>>,
And<APPayment.docType, In3<APDocType.check, APDocType.prepayment, APDocType.quickCheck>>>>.Select(Base))
{
yield return new PXResult<APPayment, Vendor, APContact>(doc, doc, doc);
if (_copies.ContainsKey((APPayment)doc))
{
_copies.Remove((APPayment)doc);
}
_copies.Add((APPayment)doc, PXCache<APPayment>.CreateCopy(doc));
}
}
private bool cleared;
public void Clear()
{
Base.Filter.Current.CurySelTotal = 0m;
Base.Filter.Current.SelTotal = 0m;
Base.Filter.Current.SelCount = 0;
cleared = true;
Base.Clear();
}
private readonly Dictionary<object, object> _copies = new Dictionary<object, object>();
}
Per Rick's suggestion I implemented the FieldSelecting method. Much, much simpler/cleaner code. It does in fact cause a round trip to the database for each row when using this in a grid column, however, for check printing this should be acceptable. Thanks Rick! Code below.
protected void APPayment_UsrRemitTo_FieldSelecting(PXCache cache, PXFieldSelectingEventArgs e)
{
var row = (APPayment)e.Row;
// fill usrRemitTo from APContact
if (row != null)
{
var extension = PXCache<APRegister>.GetExtension<APRegisterExt>(row);
using (PXConnectionScope cs = new PXConnectionScope())
{
APContact rec = PXSelectReadonly<APContact, Where<APContact.contactID, Equal<Required<APPayment.remitContactID>>>>.Select(Base, row.RemitContactID);
if (rec != null)
{
string remitToName = (!string.IsNullOrEmpty(rec.FullName)) ? rec.FullName : "";
e.ReturnValue = remitToName;
}
else
{
e.ReturnValue = "";
}
}
}
}
Using DBScalar, you can further simplify your event with just one line :
public class APPaymentExt : PXCacheExtension<APPayment>
{
#region UsrRemitTo
[PXString(100)]
[PXUIField(DisplayName="Remit To")]
[PXDBScalar(typeof(Search<APContact.fullName,Where<APContact.contactID, Equal<APPayment.remitContactID>>>))]
public virtual string UsrRemitTo { get; set; }
public abstract class usrRemitTo : PX.Data.BQL.BqlString.Field<usrRemitTo> { }
#endregion
}

How to get DAC record from Note Table

Does anyone have a code snippet on how to go from RefNoteId => DAC when I dont know what dac type the note is attached to?
I have made it this far (row.RefNoteID is what I am starting from)
Note note = PXSelect<Note, Where<Note.noteID, Equal<Required<Note.noteID>>>>.Select(this, row.RefNoteID);
Type recordType = Type.GetType(note.EntityType);
PXCache recordCache = Caches[recordType];
How can I now do a PXSelect<recordType, Where<recodType.noteID, Equal<Required<recordType.noteID>>>>.Select(GRAPH) ? The recordType could be any DAC in the system that has a noteID.
Thanks
The below code works for me and it is based on the way Acumatica gets the record inside the PXRefNoteSelectorAttribute.PrimaryRow_RowPersisted.
The problem with this approach is that this will work for Header entities like SOOrder, INRegister, SOInvoice, SOShipment, and others. But for "detail" entities like SOLine, INTran, and others this approach will work only if that corresponding record has some Note related to Text/File. Acumatica is adding records corresponding to their NoteID into the Note table only if that detail records have some Note/Text. My best guess is that this is done in order to avoid over-spamming the Note table.
using PX.Data;
using PX.Objects.SO;
using System;
using System.Collections;
using System.Linq;
using System.Web.Compilation;
namespace SearchByNoteID
{
// Acuminator disable once PX1016 ExtensionDoesNotDeclareIsActiveMethod extension should be constantly active
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> searchByNoteID;
[PXUIField(DisplayName ="Search by Note ID")]
[PXButton(CommitChanges = true)]
public virtual IEnumerable SearchByNoteID(PXAdapter adapter)
{
var order = adapter.Get<SOOrder>().FirstOrDefault();
if(order!=null)
{
//
//...
//
Guid? noteID = GetNoteID();
object record = GetRecordByNoteID(noteID);
//
//... do whatever you want with the record
//
}
return adapter.Get();
}
protected object GetRecordByNoteID(Guid? noteID)
{
var type = GetEntityType(this.Base, noteID);
if(type==null) return null;
object entityRow = new EntityHelper(this.Base).GetEntityRow(type, noteID);
return entityRow;
}
protected Type GetEntityType(PXGraph graph, Guid? noteID)
{
if (noteID == null)
{
return null;
}
Note note = PXSelectBase<Note, PXSelect<Note, Where<Note.noteID, Equal<Required<Note.noteID>>>>.Config>.SelectWindowed(graph, 0, 1, new object[]
{
noteID
});
if (note == null || string.IsNullOrEmpty(note.EntityType))
{
return null;
}
return PXBuildManager.GetType(note.EntityType, false);
}
}
}

How to update a custom field values while confirm shipment action is running in Acumatica

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

Acumatica - Need Help in Updating Activities in Project Quotes Screen PM304500 through custom action

I have a custom action on screen CR304000 - OpportunityMaint in the Quotes tab of the grid view that marks a field called IsPrimary in the CRQuote DAC as true for the current record in the Quotes view. These project Quotes are associated with the current opportunity as well as with a related PMQoute in the PMQuoteMaint BLC. The PMQuoteMaint BLC has a view called Activties that has all the CRActivities associated with the PMQuote. I created a custom field in the CRActivity called IsPrimary and added it to the Activities view in the PMQuoteMaint BLC grid.
My goal was to override the Action in the OpportunityMaint to update the IsPrimary field in CRActivity to true or false depending on what the Action in OpportunityMaint is toggling. However, my IsPrimary field in PMQuoteMaint is not toggling. My code attempts to get Current Quote OpportunityMain and then create a PMQuoteMaint graph and set the Current record. Then iterate though Activies view and set the IsPrimary field accordingly. Like I said, not having success because I'm not sure that my code is successfully retrieving the correct Activities.
There might be a better way to access CRActivity associated with a PMQuote, but I'm not sure. Any help would be appreciated. Here is code:
public virtual IEnumerable PrimaryQuote(PXAdapter adapter)
{
foreach (CROpportunity opp in adapter.Get())
{
if (Quotes.Current?.IsPrimary != true)
{
var selectExistingPrimary = new PXSelect<CRQuote, Where<CRQuote.quoteID,
Equal<Required<CRQuote.quoteID>>>>(this);
CRQuote primary = selectExistingPrimary.Select(opp.DefQuoteID);
if (primary != null && primary.QuoteID != Quotes.Current.QuoteID && primary.Status ==
PM.PMQuoteStatusAttribute.Closed)
{
throw new PXException(PM.Messages.QuoteIsClosed, opp.OpportunityID,
primary.QuoteNbr);
}
var quoteID = Quotes.Current.QuoteID;
var opportunityID = this.Opportunity.Current.OpportunityID;
this.Persist();
PXDatabase.Update<Standalone.CROpportunity>(
new PXDataFieldAssign<Standalone.CROpportunity.defQuoteID>(quoteID),
new PXDataFieldRestrict<Standalone.CROpportunity.opportunityID>(PXDbType.VarChar,
255, opportunityID, PXComp.EQ)
);
this.Cancel.Press();
CROpportunity rec = this.Opportunity.Search<CROpportunity.opportunityID>
(opportunityID);
yield return rec;
}
yield return opp;
}
``` My OverRide
public PXAction<CROpportunity> primaryQuote;
[PXUIField(DisplayName = Messages.MarkAsPrimary)]
[PXButton]
public virtual IEnumerable PrimaryQuote(PXAdapter adapter)
{
// this is currently selected record in quotes grid
var currQuoteNbr = Base.Quotes.Current.QuoteNbr;
bool isPrimary2;
foreach (CRQuote quote in Base.Quotes.Select())
{
var quoteNbr = quote.QuoteNbr;
if(quoteNbr.Trim() == currQuoteNbr.Trim())
{
isPrimary2 = true;
}
else
{
isPrimary2 = false;
}
PXTrace.WriteInformation(string.Format("Quote: {0} Value:
{1}",quoteNbr.ToString(),isPrimary2.ToString()));
var PMQuoteMaintGraph = PXGraph.CreateInstance<PMQuoteMaint>();
PMQuoteMaintGraph.Quote.Current = PMQuoteMaintGraph.Quote.Search<PMQuote.quoteNbr>
(quoteNbr.Trim()); // this is the current quote
foreach (CRActivity activity in PMQuoteMaintGraph.Activities.Select())
{
CRActivityExt itemExt = PXCache<CRActivity>.GetExtension<CRActivityExt>(activity);
itemExt.UsrPrimary = isPrimary2;
PMQuoteMaintGraph.Activities.Cache.Persist(PXDBOperation.Update);
PXDatabase.Update<CRActivity>(new PXDataFieldAssign<CRActivityExt.usrPrimary>
(isPrimary2));
}
}
return Base.primaryQuote.Press(adapter);
}

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();
}
}
}
}

Resources