Skip to main content

I have a strange problem. Due to a conflict (I guess) with a 3rd party vendor, my customer sometimes experiences very odd behavior. 

What happens is when they create an appointment from a service order, and save the appointment, it changes the UnitPrice on the Sales Order Detail.

I know that sounds weird… but, in example:

If line 3 of the sales order detail has a unitPrice of $20, and they add an appointment, its detail line 3 also shows a UnitPrice of $20. But when they save the appointment, the service order line 3 now has a price of $34.95. 

It doesn’t always happen. When it happens the price is not always the same amount. It’s not always the same inventory item. But it happens enough to be a problem.

Fortunately, we have a data set that, when following the same steps, will show the same problem every time. So I have to run it, roll it back, run it again, etc. Long process to debug, but at least it is repeatable.

That is why I know it has something to do with a conflict elsewhere. I can put a debugger on the RowInserting event, and the price is $20. At the start of RowInserted, it is still $20. But, coming out of RowInserted, it has inexplicitly changed. There is nothing in my code that changes it... It happens somewhere else. But, it does not happen if I unpublish my code. It also does not happen if I only publish my code.

So, unable to trace it to ground, I am trying a hack-y fix. When the appointment is entered, on the (Events.RowPersisted<FSAppointment> e) event, I want to check the unitPrice, correct it if necessary, and then save it back out with the correct value.

And, I want to save the changes by calling the pressSave event in the ServiceOrderEntry Graph, because it does total and tax calculations, and I don’t want to have to code all of that by hand.

I have code that runs fine, but the update is not working and I am at a loss as to why. Here is the code:

        protected virtual void _(Events.RowPersisted<FSAppointment> e, PXRowPersisted BaseHandler)
{
var accessScreen = base.Base.Accessinfo.ScreenID;

var fsAppointmentDetCache = Base.CachesBtypeof(FSAppointmentDet)];
var fsSODetCache = Base.CachesBtypeof(FSSODet)];
// Acuminator disable once PX1045 PXGraphCreateInstanceInEventHandlers nJustification]
if (serviceOrderGraph == null)
{
serviceOrderGraph = GetNewServiceOrderEntryGraph(clearGraph: true);
}
// Capture original values before any changes are persisted
string onRefNbr = e.Row.SORefNbr;
string onSoType = e.Row.SrvOrdType;
FSServiceOrder onServiceOrder = FSServiceOrder.PK.Find(e.Cache.Graph, onSoType, onRefNbr);
List<LinePrice> linePrices = new List<LinePrice>();
foreach (FSSODet line in fsSODetCache.Cached)
{
LinePrice lp = new LinePrice
{
LineNbr = line.LineNbr.Value,
Price = line.UnitPrice.GetValueOrDefault(),
SoType = onSoType,
RefNbr = onRefNbr
};
linePrices.Add(lp);
}
// ensure the Base method logic is executed:
BaseHandler?.Invoke(e.Cache, e.Args);
List<int?> ChangedLines = new List<int?>();
foreach (LinePrice lp in linePrices)
{
// Find the corresponding FSSODet row based on LineNbr.
FSSODet matchingDetail = PXSelect<FSSODet,
Where<FSSODet.srvOrdType, Equal<Required<FSSODet.srvOrdType>>,
And<FSSODet.refNbr, Equal<Required<FSSODet.refNbr>>,
And<FSSODet.lineNbr, Equal<Required<FSSODet.lineNbr>>>>>>
.Select(serviceOrderGraph, lp.SoType, lp.RefNbr, lp.LineNbr);
serviceOrderGraph.ServiceOrderRecords.Current = onServiceOrder;

if (matchingDetail != null && matchingDetail.UnitPrice != lp.Price)
{
ChangedLines.Add(matchingDetail.LineNbr);
// If the prices are different, update the UnitPrice.
matchingDetail.UnitPrice = lp.Price;
serviceOrderGraph.ServiceOrderDetails.Cache.Update(matchingDetail);
}
}
// Save changes after updating all necessary lines.
// Acuminator disable once PX1043 SavingChangesInEventHandlers nJustification]
serviceOrderGraph.Save.Press();
// Confirm changes after updating all necessary lines.
foreach (int? linnbr in ChangedLines)
{
var FSDetail = FSSODet.PK.Find(serviceOrderGraph, onSoType, onRefNbr, linnbr);
if (FSDetail != null)
{
var chkPrice = FSDetail.UnitPrice;
int i = 0;
}
}
}

 It is a little wonky because I added some things in there for debugging. This code uses a simple class:

 

    internal class LinePrice
{
internal decimal? Price;
internal int? LineNbr;
internal String RefNbr;
internal String SoType;

public LinePrice() { }
}

And I also copied GetServiceOrderEntry from the Acumatica source code to get the ability to create a ServiceOrderENtry graph from within AppointmentEnrty.

        internal ServiceOrderEntry GetNewServiceOrderEntryGraph(bool clearGraph)
{
ServiceOrderEntry _ServiceOrderEntryGraph = null; // this will always make new

if (_ServiceOrderEntryGraph == null)
{
_ServiceOrderEntryGraph = PXGraph.CreateInstance<ServiceOrderEntry>();
}
else if (clearGraph == true && _ServiceOrderEntryGraph.RunningPersist == false)
{
_ServiceOrderEntryGraph.Clear();
}

if (_ServiceOrderEntryGraph.RunningPersist == false)
{
_ServiceOrderEntryGraph.RecalculateExternalTaxesSync = true;
_ServiceOrderEntryGraph.GraphAppointmentEntryCaller = this.Base;
}

return _ServiceOrderEntryGraph;
}

Any help would be greatly appreciated. The only other thing I know to try is using PXDatabase.Update() -- which WILL update it, but then I have to code the totaling and tax totals myself which I REALLY don’t want to do.

 

If I understand how Acumatica works (and specifically caches), your issue is the following:

Here you are running a select on the database to find a record:

// Find the corresponding FSSODet row based on LineNbr
FSSODet matchingDetail = PXSelect<FSSODet, Where<FSSODet.srvOrdType, Equal<Required<FSSODet.srvOrdType>>, And<FSSODet.refNbr, Equal<Required<FSSODet.refNbr>>, And<FSSODet.lineNbr, Equal<Required<FSSODet.lineNbr>>>>>> .Select(serviceOrderGraph, lp.SoType, lp.RefNbr, lp.LineNbr);

This is fine to GET information. To SET information, like you’re doing here

matchingDetail.UnitPrice = lp.Price;

you would need to be updating the record in the cache. Since you selected matchingDetail straight from the database, it isn’t in the cache. There is an identical record in the cache, but it’s not this one. Therefore, calling the cache to update isn’t doing anything.

 

To get the record from the cache, I would suggest doing this instead:

// Find the corresponding FSSODet row based on LineNbr
FSSODet matchingDetail = serviceOrderGraph.ServiceOrderDetails.Select()?.FirstTableItems.Where(i => i.LineNbr == lp.LineNbr);

Now calling

serviceOrderGraph.ServiceOrderDetails.Update(matchingDetail);

should update the record in the cache.


 

To get the record from the cache, I would suggest doing this instead:

// Find the corresponding FSSODet row based on LineNbr
FSSODet matchingDetail = serviceOrderGraph.ServiceOrderDetails.Select()?.FirstTableItems.Where(i => i.LineNbr == lp.LineNbr);

Now calling

serviceOrderGraph.ServiceOrderDetails.Update(matchingDetail);

should update the record in the cache.

Daryl, thanks for the response!

I believe you are right… although the code you gave me uses the serviceOrderGraph, which I actually create in my code (I didn’t include that part, so you couldn’t have known) And I don’t think that will work, since it is empty (ie no FSServiceOrder or FSSODet data in it).

BTW… In my code I had this line: 

 

    serviceOrderGraph.ServiceOrderRecords.Current = onServiceOrder;

Which I was trying to do in order to get the values in there so I could update them… but, the issue is How can I get my logic called from a loaded ServiceOrderEntry graph, that has the service order and FSSODet in it so they can be updated by the base business logic? (ie tax totals, etc)? 

 

I don’t seem to be able to do that… I don;t see how I can assign the FSSODet data. (meaning there is no serviceOrderGraph.ServiceOrderDetailRecords.Current option)

All I can think of is to serialize the objects, write them out to a SQL database table, and then call them back in so I can create a ServiceOrderEntry graph, and load them in there before doing the update… But I might just be too frazzled at this point to be thinking straight… Any ideas?

 


Usually, you populate a graph by doing this, exactly like you are:

serviceOrderGraph.ServiceOrderRecords.Current = onServiceOrder;

This should fill the tabs with data. After that, it's just a matter of changing and updating the cache records. Did you try plugging in the code? I think it should work replacing yours with mine. If not, I'm misunderstanding something.


Usually, you populate a graph by doing this, exactly like you are:

serviceOrderGraph.ServiceOrderRecords.Current = onServiceOrder;

This should fill the tabs with data. After that, it's just a matter of changing and updating the cache records. Did you try plugging in the code? I think it should work replacing yours with mine. If not, I'm misunderstanding something.

I think I may have gone down a rabbit hole, and taken you with me…

The issue seems to be that the problem happens after it leaves my code. I put the code you suggest in the Persist event (I have tried it both at the graph level and the row event) but the problem doesn’t happen, so there is nothing to change.

Again, the thing I am trying to fix is that sometimes, somehow, when an appointment is added, the line item on the original Service Order has its UnitPrice changed. 

I was trying to write code that captured the correct value, executed BaseHandler?.Invoke(e.Cache, e.Args), and then rewrote the FSSSDet with the correct (old) UnitPrice value… but all this is happening after it leaves my code, so there is nothing to correct yet (and why I thought the update wasn’t working.)

So… sorry for the confusion. I am going to have to figure something else out. 

This is really frustrating. I wish the order that events got executed matched the Customization level number. That way, I could always go last. But, apparently there is no guarantee of which customization fires first.

Thanks for looking at it Daryl!

 


Reply