How to solve "Changes cannot be saved to the database from the event handler"? - acumatica

In POOrderEntry as a POLine is created or deleted, I need to push a reference back to a custom DAC that originates the PO Line. For instance, if the PO Line is deleted, my custom DAC has the reference removed in Events.RowDeleted via:
using (PXTransactionScope ts = new PXTransactionScope())
{
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pOType>(row, null);
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pONbr>(row, null);
Base.Caches[typeof(MyDAC)].SetValueExt<MyDAC.pOLineNbr>(row, null);
Base.Caches[typeof(MyDAC)].Update(row);
Base.Caches[typeof(MyDAC)].Persist(PXDBOperation.Update);
ts.Complete(Base);
}
I have tried to allow the normal Persist to save the values, but it doesn't unless I call Persist (last line of my example above). The result is an error via Acuminator of "Changes cannot be saved to the database from the event handler". As I look at this, I wonder if it should be in an Long Operation instead of a Transaction Scope, but the error from Acuminator tells me I'm doing this wrong. What is the proper way to achieve my update back to "MyDAC" for each PO Line?
I have also tried initializing a graph instance for MyDAC's graph, but I get a warning about creating a PXGraph in an event handler so I can't "legally" call the graph where MyDAC is maintained.
My code compiles and functions as desired, but the error from Acuminator tells me there must be a more proper way to accomplish this.

You can add a view to the graph extension.
Then in the row deleted you will use your view.Update(row) to update your custom dac.
During the base graph persist your records will commit as long as there are no other errors found in other events.
The way you have it now commits your changes with a chance the row that was being deleted is never deleted.
Also with this change there is no need to use PXTransactionScope.
An example might look something like this...
public class POOrderEntryExtension : PXGraphExtension<POOrderEntry>
{
public PXSelect<MyDac> MyView;
protected virtual void _(Events.RowDeleted<POLine> e)
{
//get your row to update from e.Row
var myRow = PXSelect...
myRow.pOType = null;
myRow.pONbr = null;
myRow.pOLineNbr = null;
MyView.Update(myRow);
}
}

Related

How do I attach data to custom fields on INTranCost during release of POReceiptEntry?

I need to attach custom data into new fields added to INTranCost when the PO Receipt occurs.
Following the breadcrumbs, it seems that POReceiptEntry -> Release Action eventually calls INDocumentRelease.ReleaseDoc that eventually creates INTranCost. I tried extending both POReceiptEntry and INDocumentRelease to add an event for INTranCost_RowInserted to publish a PXTrace message, but the trace doesn't appear, telling me that I'm not hitting the event that I expected. (Which explains why the real business logic I need included didn't fire.)
protected virtual void _(Events.RowInserted<INTranCost> e)
{
PXTrace.WriteInformation("This is it!");
}
Of course, I want to put real code in this spot, but I am just trying to make sure I'm hitting the event properly. This works on pretty much everything else I've done, including attaching similar data to INTranExt fields. I cannot get it to work for INTranCost so that I can add to INTranCostExt. At this point, I can't determine if it is location (which graph extension) or a special methodology required for this special case.
I also tried overriding events and putting a breakpoint on the code, but it's like I'm not even on the same process. (Yes, I checked that I am connected to the right Acumatica instance and that I have no errors.)
What event in which graph is required to capture the creation in INTranCost for a PO Receipt to update custom fields in INTranCostExt?
Using Request Profiler, I was able to determine that I was close but not deep enough. While the INTranCost object to insert was built in INDocumentRelease FILE, the actual insert was processed in INReleaseProcess graph in that same file.
I only need to execute this "push" from the data captured on the POLine when the INTranCost record is created, and LineNbr is a key field and therefore never updated after it is set. I need to be sure that I have enough data to make the connection back, and the primary key links me back to the INTran easily. That subsequently gets back to the POReceiptLine to the POLine where the data is maintained that needs the "current value" to be captured when the transaction is posted. Since I need to update the DAC Extension, I need to use an event that will allow an existing DAC.Update to apply my values. Therefore, I added an event handler on INTranCost_LineNbr_FieldUpdated since that value should not be "updated" after it is set initially.
Code that accomplished the task:
public class INReleaseProcess_Extension : PXGraphExtension<INReleaseProcess>
{
public override void Initialize()
{
base.Initialize();
}
protected virtual void _(Events.FieldUpdated<INTranCost.lineNbr> e)
{
INTranCost row = (INTranCost) e.Row;
INTran tran = PXSelect<INTran,
Where<INTran.docType, Equal<Required<INTran.docType>>,
And<INTran.refNbr, Equal<Required<INTran.refNbr>>,
And<INTran.lineNbr, Equal<Required<INTran.lineNbr>>
>>>>
.SelectSingleBound(Base, null, row.DocType, row.RefNbr, (int?) e.NewValue);
if (tran?.POReceiptType != null && tran?.POReceiptNbr != null)
{
PXResultset<POReceiptLine> Results = PXSelectJoin<POReceiptLine,
InnerJoin<POLine, On<POLine.orderType, Equal<POReceiptLine.pOType>,
And<POLine.orderNbr, Equal<POReceiptLine.pONbr>,
And<POLine.lineNbr, Equal<POReceiptLine.pOLineNbr>>>>,
InnerJoin<POOrder, On<POOrder.orderType, Equal<POLine.orderType>,
And<POOrder.orderNbr, Equal<POLine.orderNbr>>>>>,
Where<POReceiptLine.receiptType, Equal<Required<POReceiptLine.receiptType>>,
And<POReceiptLine.receiptNbr, Equal<Required<POReceiptLine.receiptNbr>>,
And<POReceiptLine.lineNbr, Equal<Required<POReceiptLine.lineNbr>>>>>>.
SelectSingleBound(Base, null, tran.POReceiptType, tran.POReceiptNbr, tran.POReceiptLineNbr);
if (Results != null)
{
foreach (PXResult<POReceiptLine, POLine, POOrder> result in Results)
{
POReceiptLine receipt = result;
POLine line = result;
POOrder order = result;
POLineExt pOLineExt = PXCache<POLine>.GetExtension<POLineExt>(line);
INTranCostExt iNTranCostExt = PXCache<INTranCost>.GetExtension<INTranCostExt>(row);
if (pOLineExt != null && iNTranCostExt != null)
{
Base.Caches[typeof(INTranCost)].SetValueExt<INTranCostExt.usrField>(row, pOLineExt.UsrField);
}
}
}
}
}
}

How do I capture the PO references assigned into SOLineSplit3 during POCreate for an SO?

I have a modification that facilitates an internal business process utilizing PO and SO. The screen provides the ability to purchase an MRO spare part outside of the normal replenishment process. The item may or may not be maintained in inventory, so it may be ordered for replenishment or as an SO to be processed as Mark for PO.
In creating the SO, I am able to store the SO reference information to the DAC for my customization. When creating the PO directly, I also am able to capture the PO reference information. However, when creating the PO from the SO using the standard Acumatica menu action for Create Purchase Order, I have been unable to capture the right event to enable storing the PO reference being assigned in SOLineSplit3 to my custom DAC. (Worth noting that I also need to be able to override the default curyunitcost value on the PO line using the value stored on my custom DAC as these purchases do not carry a fixed price per buy. This is done by tracing the SOLineSplit back to my custom DAC and overriding POLine_CuryUnitCost_FieldDefaulting.)
The action invoked on the Sales Order Entry screen (Action - Create Purchase Order) calls the method CreatePOOrders in POCreate.cs which in turn creates an instance of the POOrderEntry graph to create the actual purchase order.
Eventually, the following code is reached, which appears to attach the PO reference information to SOLineSplit3 as soline:
soline.POType = line.OrderType;
soline.PONbr = line.OrderNbr;
soline.POLineNbr = line.LineNbr;
soline.RefNoteID = docgraph.Document.Current.NoteID;
docgraph.UpdateSOLine(soline, docgraph.Document.Current.VendorID, true);
docgraph.FixedDemand.Cache.SetStatus(soline, PXEntryStatus.Updated);
I am not yet familiar with Cache.SetStatus, but the pop-up description seems to indicate that this is using the FixedDemand select in POOrderEntry to find and set (or insert) the SOLineSplit3 record. The call to UpdateSOLine above it is a new internal method that was not in my previous version of POCrete.cs, as this entire method seems to have had some significant rework recently. In trying to capture events on SOLineSplit3 in both POCreate and POOrderEntry, it appears that Cache.SetStatus does not raise any events that I can capture... or I am just completely lost on what event to capture/override.
Immediately following this section, the following appears to update a Replenishment record and save the entire POOrderEntry graph.
if (docgraph.Transactions.Cache.IsInsertedUpdatedDeleted)
{
using (PXTransactionScope scope = new PXTransactionScope())
{
docgraph.Save.Press();
if (demand.PlanType == INPlanConstants.Plan90)
{
docgraph.Replenihment.Current = docgraph.Replenihment.Search<INReplenishmentOrder.noteID>(demand.RefNoteID);
if (docgraph.Replenihment.Current != null)
{
INReplenishmentLine rLine =
PXCache<INReplenishmentLine>.CreateCopy(docgraph.ReplenishmentLines.Insert(new INReplenishmentLine()));
rLine.InventoryID = line.InventoryID;
...
rLine.PlanID = demand.PlanID;
docgraph.ReplenishmentLines.Update(rLine);
docgraph.Caches[typeof(INItemPlan)].Delete(demand);
docgraph.Save.Press();
}
}
scope.Complete();
}
...
}
Basically, I need to insert my code right between the assignment of the PO information to "soline" and the docgraph.Save.Press(); without copying dozens of lines of code to modify this method. I have managed cloning the base method and inserting my code successfully, but I'd prefer to use an event handler and eliminate modifying the standard code. But the question... What event in which graph will let me grab the PO information and follow the breadcumbs back through SOLineSplit to my custom DAC?
Acumatica Build 18.212.0033
Extend POCreate graph because it is the one instanciating the POOrderEntry you are interested in.
Setup a hook on any POOrderEntry graph created by POCreate and subscribe your events on the intercepted graph. I tested this solution, with a Sales Order that has allocations lines in allocation window it will catch the SOLineSplit3 events:
public class POCreate_Extension : PXGraphExtension<POCreate>
{
public override void Initialize()
{
PXGraph.InstanceCreated.AddHandler<POOrderEntry>((graph) =>
{
graph.RowInserting.AddHandler<POOrder>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_POOrder_RowInserting");
});
graph.RowInserting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowInserting");
});
graph.RowUpdating.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowUpdating");
});
graph.RowPersisting.AddHandler<POOrderEntry.SOLineSplit3>((sender, e) =>
{
PXTrace.WriteInformation("POOrderEntry_SOLineSplit3_RowPersisting");
});
});
}
}

How to automatically create Note record in Acumatica?

I've noticed that whenever an AR Invoice gets saved, a record gets created in the Note table with the new invoice's note ID. Can you tell me how that is being accomplished? I'd like to get one of my screens to do the same thing. I guess there must be some kind of attribute on the either the DAC or the graph but I can't find it. I have the PXNote attribute on the NoteID column in my DAC but it does not cause a Note record to be automatically created.
Thanks for your help.
To have Note record automatically created when a new parent record gets saved, one should invoke the static PXNoteAttribute.GetNoteID<Field>(PXCache cache, object data) method when the parent record is inserted in the cache.
For example, to have Note record automatically created when a new Stock Item gets saved, you should subscribe to RowInserted handler for the InventoryItem DAC and call PXNoteAttribute.GetNoteID<Field>(...):
public class InventoryItemMaintExt : PXGraphExtension<InventoryItemMaint>
{
public void InventoryItem_RowInserted(PXCache sender, PXRowInsertedEventArgs e)
{
var noteCache = Base.Caches[typeof(Note)];
var oldDirty = noteCache.IsDirty;
PXNoteAttribute.GetNoteID<InventoryItem.noteID>(sender, e.Row);
noteCache.IsDirty = oldDirty;
}
}
The code snippet above can be incorporated into almost any custom BLC with a couple simple changes to replace InventoryItem with a custom DAC.

Error #153 Another process has added 'Note' record. Your changes will be lost(SalesOrderEntry)

In the Sales Order page, I created a custom button which purpose is to save and refresh the page. Currently it saves fine and processes the new order to an order number but when I try to add an item/edit or perform an action in the drop down menu I receive the error message.
Here's my code:
public PXAction<SOOrder> SRefresh;
[PXUIField(DisplayName = "S RefreshT")]
[PXButton(CommitChanges = true)]
protected virtual IEnumerable sRefresh(PXAdapter adapter)
{
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
Base.Actions.PressSave();
SOLine sLine = PXSelect<SOLine, Where<SOLine.orderNbr, Equal<Required<SOLine.orderNbr>>>>.Select(graph, this.Base.Document.Current.OrderNbr);
if (sLine != null && sLine.InventoryID.HasValue)
{
graph.Document.Current = graph.Document.Search<SOLine.orderNbr>(sLine.OrderNbr);
throw new PXRedirectRequiredException(graph, null);
}
return adapter.Get();
}
I've also tried using graph.Persist() as said in the manual instead of Action.PressSave(); with no success.
I appreciate any input you guys may have, Thank you
Since you're working with the current sales order, you don't need to create a new instance of the sales order entery graph and redirect your user. You can work with the Base object and run all your logic on it.
Base.Document.Current contains a reference to the current SOOrder, and Base.Transactions contains the list of SOLine of this document. Another problem I also found in your code is that you're calling Document.Search<SOline.orderNbr>; it should be SOOrder.orerNbr since you're searching inside the Document view, which contains sales orders, and not lines. In this case, it's not even necessary to search, Base.Document.Current will already be set to the order you're looking at.
I strongly recommend completing the standard Acumatica developer trainings - T100, T200, T300; this stuff is all covered and will get you productive quickly

Subsonic - Where do i include my busines logic or custom validation

Im using subsonic 2.2
I tried asking this question another way but didnt get the answer i was looking for.
Basically i ususally include validation at page level or in my code behind for my user controls or aspx pages. However i haev seen some small bits of info advising this can be done within partial classes generated from subsonic.
So my question is, where do i put these, are there particular events i add my validation / business logic into such as inserting, or updating. - If so, and validation isnt met, how do i stop the insert or update. And if anyone has a code example of how this looks it would be great to start me off.
Any info greatly appreciated.
First you should create a partial class for you DAL object you want to use.
In my project I have a folder Generated where the generated classes live in and I have another folder Extended.
Let's say you have a Subsonic generated class Product. Create a new file Product.cs in your Extended (or whatever) folder an create a partial class Product and ensure that the namespace matches the subsonic generated classes namespace.
namespace Your.Namespace.DAL
{
public partial class Product
{
}
}
Now you have the ability to extend the product class. The interesting part ist that subsonic offers some methods to override.
namespace Your.Namespace.DAL
{
public partial class Product
{
public override bool Validate()
{
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.Errors.Add("ProductName cannot be empty");
return Errors.Count == 0;
}
// another way
protected override void BeforeValidate()
{
if (string.IsNullOrEmpty(this.ProductName))
throw new Exception("ProductName cannot be empty");
}
protected override void BeforeInsert()
{
this.ProductUUID = Guid.NewGuid().ToString();
}
protected override void BeforeUpdate()
{
this.Total = this.Net + this.Tax;
}
protected override void AfterCommit()
{
DB.Update<ProductSales>()
.Set(ProductSales.ProductName).EqualTo(this.ProductName)
.Where(ProductSales.ProductId).IsEqualTo(this.ProductId)
.Execute();
}
}
}
In response to Dan's question:
First, have a look here: http://github.com/subsonic/SubSonic-2.0/blob/master/SubSonic/ActiveRecord/ActiveRecord.cs
In this file lives the whole logic I showed in my other post.
Validate: Is called during Save(), if Validate() returns false an exception is thrown.
Get's only called if the Property ValidateWhenSaving (which is a constant so you have to recompile SubSonic to change it) is true (default)
BeforeValidate: Is called during Save() when ValidateWhenSaving is true. Does nothing by default
BeforeInsert: Is called during Save() if the record is new. Does nothing by default.
BeforeUpdate: Is called during Save() if the record is new. Does nothing by default.
AfterCommit: Is called after sucessfully inserting/updating a record. Does nothing by default.
In my Validate() example, I first let the default ValidatColumnSettings() method run, which will add errors like "Maximum String lenght exceeded for column ProductName" if product name is longer than the value defined in the database. Then I add another errorstring if ProductName is empty and return false if the overall error count is bigger than zero.
This will throw an exception during Save() so you can't store the record in the DB.
I would suggest you call Validate() yourself and if it returns false you display the elements of this.Errors at the bottom of the page (the easy way) or (more elegant) you create a Dictionary<string, string> where the key is the columnname and the value is the reason.
private Dictionary<string, string> CustomErrors = new Dictionary<string, string>
protected override bool Validate()
{
this.CustomErrors.Clear();
ValidateColumnSettings();
if (string.IsNullOrEmpty(this.ProductName))
this.CustomErrors.Add(this.Columns.ProductName, "cannot be empty");
if (this.UnitPrice < 0)
this.CustomErrors.Add(this.Columns.UnitPrice, "has to be 0 or bigger");
return this.CustomErrors.Count == 0 && Errors.Count == 0;
}
Then if Validate() returns false you can add the reason directly besides/below the right field in your webpage.
If Validate() returns true you can safely call Save() but keep in mind that Save() could throw other errors during persistance like "Dublicate Key ...";
Thanks for the response, but can you confirm this for me as im alittle confused, if your validating the column (ProductName) value within validate() or the beforevalidate() is string empty or NULL, doesnt this mean that the insert / update has already been actioned, as otherwise it wouldnt know that youve tried to insert or update a null value from the UI / aspx fields within the page to the column??
Also, within asp.net insert or updating events we use e.cancel = true to stop the insert update, if beforevalidate failes does it automatically stop the action to insert or update?
If this is the case, isnt it eaiser to add page level validation to stop the insert or update being fired in the first place.
I guess im alittle confused at the lifecyle for these methods and when they come into play

Resources