Acumatica Unbound User Fields on SOOrder - acumatica

I have a user field on an SOOrder DAC extension, which is a sum of some of the lines on the document (based on a field in the SOLine extension). When I add a new line, the total is updating properly. However, when I load the document for the first time, the screen is showing 0.00. I created an SOOrderEntry extension and I put code into the SOLine_RowSelecting event handler. When I load the document, it steps into the code and it looks like it is setting the fields properly, but they don't show on the screen. The same method is called from the SOLine_CuryLineAmt_FieldUpdated, and that works just fine. Here is the code I'm using:
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
//Used to prevent recursive calls in RowSelecting
bool _isCalculating = false;
protected virtual void SOLine_RowSelecting(PXCache cache, PXRowSelectingEventArgs e)
{
var row = e.Row as SOLine;
if (row == null) return;
using (new PXConnectionScope())
{
if (!_isCalculating)
CalcTotals();
}
}
protected virtual void SOLine_CuryLineAmt_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e)
{
if (!_isCalculating)
CalcTotals();
}
public void CalcTotals()
{
SOOrder order = Base.CurrentDocument.Select();
if (order == null) return;
_isCalculating = true;
var orderExt = order.GetExtension<SOOrderExt>();
orderExt.UsrMyCustomField = 0m;
//Get totals
foreach (SOLine lineSum in Base.Transactions.Select())
{
var lineSumExt = lineSum.GetExtension<SOLineExt>();
if (lineSumExt.UsrMyCondition)
orderExt.UsrMyCustomField += lineSum.CuryLineAmt;
}
_isCalculating = false;
}
}

RowSelected is called on each callback to select the data. There's no need to re-calculate on FieldUpdated event too because RowSelected will be called when updating records. Therefore consider removing SOLine_CuryLineAmt_FieldUpdated
You have the RowSelected event declared for SOLine DAC. The event then selects all SOLine to compute the totals. This amount to when selecting one of the Detail compute the total of all Detail, that smells lack a recursive pattern. Therefore consider declaring RowSelected on the Master document which is SOOrder in this case and remove all the workarounds you have to break recursion.
There's no null check in computations. Acumatica DAC fields are nullable. With your code you can end up in situation where you add null to a number which would results in type violation at runtime. Therefore consider checking if CuryLineAmt is null before using it's value to compute the total.
You are accumulating the total in the UsrMyCustomField DAC field
using the += addition assignment operator. It works but I would
advise against that. The DAC fields aren't meant as register for
computations or temporary value place-holder. Therefore consider
accumulating the total in a local variable and assign only the final
computed value to the DAC field.
Code to compute a total with all these points considered:
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public void SOOrder_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
SOOrder order = e.Row as SOOrder;
if (order != null)
{
SOOrderExt orderExt = order.GetExtension<SOOrderExt>();
if (orderExt != null)
{
decimal total = 0M;
foreach (SOLine line in Base.Transactions.Select())
{
total += line.CuryLineAmt.HasValue ? line.CuryLineAmt.Value : 0M;
}
orderExt.UsrMyCustomField = total;
}
}
}
}

Related

Force tax recalculation when TaxZoneID is updated inside event handler

I have a graph override for Invoices
public class ARInvoiceEntry_Extension : PXGraphExtension<ARInvoiceEntry>
and in one of the event handlers I am updating TaxZoneID, which works fine. However, the taxes do not get updated or recalculated. I have tried the approach mentioned here
cache.SetValueExt<SOOrder.taxZoneID>(order, branchLoc.VTaxZoneID);
but that doesn't work for me. I have tried it in _FieldUpdating, _FieldUpdated, and even ARInvoice_RowPersisting(PXCache cache, PXRowPersistingEventArgs e, PXRowPersisting InvokeBaseHandler) events. Any ideas on why it doesn't work? The TaxZone and rates are already in the database (we are not using Avatax).
--- edit 1 ---
Here is the code where TaxZoneID is updated
namespace PX.Objects.AR
{
public class ARInvoiceEntry_Extension : PXGraphExtension<ARInvoiceEntry>
{
#region Event Handlers
protected void ARShippingAddress_PostalCode_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e, PXFieldUpdated del)
{
// PXTrace.WriteInformation("ARShippingAddress_PostalCode_FieldUpdated");
ARShippingAddress row = e.Row as ARShippingAddress;
if (row != null) {
if (DoChangeTaxZone(row)) {
var invoice = Base.Document.Current;
if (invoice != null) {
invoice.TaxZoneID = GetTaxZoneId(row);
sender.SetValueExt<ARInvoice.taxZoneID>(invoice, invoice.TaxZoneID);
}
}
}
if (del != null)
{
del(sender, e);
}
}
#endregion
private bool DoChangeTaxZone(ARShippingAddress row)
{
// logic ...
return true;
}
private string GetTaxZoneId(ARShippingAddress row)
{
// logic ...
return "TAX-ZONE-ID";
}
}
}
When you programmatically interact with tax records using the typical methods, the tax total will not refresh properly. The Tax DAC attribute doesn't recalculate the totals by default to improve performance.
To force tax attribute refresh you need to change the tax calc mode.
Tax calc mode NoCalc does not recalculate totals. This is the default mode.
Setting tax calc mode to ManualCalc is necessary to refresh the updated tax.
Code example to update the tax amount field, you can adapt it to update tax zone.
ARInvoiceEntry invoiceMaint = PXGraph.CreateInstance<ARInvoiceEntry>();
TX.TaxAttribute.SetTaxCalc<ARTran.taxCategoryID>(invoiceMaint.Transaction.Cache, null, TX.TaxCalc.ManualCalc);
invoiceMaint.CurrentDocument.Current = invoiceMaint.Document.Search<ARInvoice.refNbr>("AR005452", ARDocType.Invoice).FirstOrDefault();
invoiceMaint.Taxes.Select();
invoiceMaint.Taxes.Current = invoiceMaint.Taxes.Search<ARTaxTran.taxID>("CAGST").FirstOrDefault();
invoiceMaint.Taxes.Cache.SetValueExt<ARTaxTran.curyTaxAmt>(invoiceMaint.Taxes.Current, 3);
invoiceMaint.Taxes.Update(invoiceMaint.Taxes.Current);
invoiceMaint.SelectTimeStamp();
invoiceMaint.Save.Press();

Acumatica - Two way update unit price to gross profit percent and back

Unable to setup unit price updating gross profit percent and back.
I have successfully created a new field for the sales order line to display gross profit percent, but now the user wants a way to adjust the percentage and update the unit price as well. All my attempts so far have apparently caused an infinite loop.
Attribute added to [UnitPrice] field in the DAC:
[PXFormula(typeof(Switch<Case<Where<SOLineExt.usrGPPercent, Equal<decimal0>>, decimal0>, Mult<SOLineExt.usrGPPercent, SOLine.curyUnitCost>>))]
[UsrGPPercent] field attributes:
[PXDBDecimal]
[PXUIField(DisplayName="GPPercent", Visible = false)]
[PXFormula(typeof(Switch<Case<Where<SOLine.curyLineAmt, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyLineAmt>>))]
[PXDefault(TypeCode.Decimal, "0.0")]
[UsrGPPct] Field attributes:
[PXUIField(DisplayName = "GP %", Enabled = true)]
[PXFormula(typeof(Mult<SOLineExt.usrGPPercent, decimal100>))]
[PXDefault(TypeCode.Decimal, "0.0")]
The above all works perfectly and update the GP% as expected.
Attempt #1, added the following attribute to [UsrGCP] (I realize the math is incomplete, just trying to proof-of-concept at this point).
[PXFormula(typeof(Switch<Case<Where<SOLine.curyLineAmt, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyLineAmt>>))]
Attempt #2: FieldUpdated handler:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (row.OrderType == "SO")
{
if (soLineExt.UsrGPPct > 0)
{
row.CuryUnitPrice = row.CuryUnitCost + (soLineExt.UsrGPPct * row.CuryUnitCost);
}
}
}
Both methods apparently resulted in an infinite loop (guessing since the debugger was triggered and IIS had to be reset). The goal is to only update once when either field is updated by the user and ignore updates made by the system. Any ideas?
Based on HB_Acumatica's response I updated the code above to:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (e.ExternalCall)
{
if (soLineExt.UsrGPPct > 0)
{
if (row.OrderType == "SO")
{
decimal NewUnitPrice;
decimal GPPercent = soLineExt.UsrGPPct ?? 0;
decimal UnitCost = row.CuryUnitCost ?? 0;
NewUnitPrice = UnitCost + ((GPPercent / (decimal)100) * UnitCost);
row.CuryUnitPrice = NewUnitPrice;
row.UnitPrice = NewUnitPrice;
}
}
}
}
This almost works. Nothing happens on the screen, but when saved the unit price does indeed update and save properly. Almost there, is there some sort of update step I'm missing?
This is usually fixed with e.ExternalCall in the field updated event handler to prevent recursion:
protected void SOLine_UsrGPPct_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if (e.ExternalCall)
{
// Make sure it gets in here only once when value change in UI
}
}
ExternalCall Property (PXRowUpdatingEventArgs)
Gets the value indicating, if it equals true, that the update of the
DAC object has been initiated from the UI or through the Web Service
API
Besides that, the other case is when two fields attributes are inter-dependent and need each other to compute their value. You can't have field 1 with a formula that depends on field 2 along with field 1 that also has a formula referencing field 2.
With that kind of setup, when asking for field 1 it will first try to evaluate the dependent field 2 in the field 1 formula and since field 2 also depends on field 1 it will start an infinite recursion loop.
There's no way out of this one that I know, you would have to re-design the DAC fields so none are mutually inter-dependent. Or just leave out the DAC attributes and perform the computation in RowSelected events (this has some disadvantages because it requires graph to work).
EDIT:
About fields values not changing in UI. This might happen when some fields need some events to be triggered when their value change (generally FieldUpdated event).
Instead of assigning values like this which doesn't raise any event:
row.Field = value;
You can use SetValueExt instead which raises events like FieldUpdated:
sender.SetValueExt<DAC.field>(row, value);
Sender needs to be cache of DAC type, in your case it should be otherwise it's:
Base.Caches[typeof(DAC)].SetValueExt<DAC.field>(row, value);
Note that a field that has a PXFormula attribute will get it's value re-computed at some point so you shouldn't manually set the value of such fields.
If you still have such an attribute on UnitPrice field you shouldn't manually update the field value:
[PXFormula(typeof(Switch<Case<Where<SOLineExt.usrGPPercent, Equal<decimal0>>, decimal0>, Mult<SOLineExt.usrGPPercent, SOLine.curyUnitCost>>))]
Maybe all you need to do is force the formula to be refreshed when UsrGPPercent changes. If that's the case, instead of setting UnitPrice value try using RaiseFieldDefaulting method to have the formula recalculated:
object dummy;
sender.RaiseFieldDefaulting<SOLine.unitPrice>(e.Row, out dummy);
Thanks so much to HB_ACUMATICA!
Final code is posted below for anyone trying to do something similar. Also cleaned up.
Two fields added to the DAC (and SalesOrder screen):
UsrGPPercent
Attributes:
[PXDBDecimal()]
[PXUIField(DisplayName = "GP %", Enabled = true, Visible = true)]
[PXFormula(typeof(Mult<Switch<Case<Where<SOLine.curyExtCost, Equal<decimal0>>, decimal0>, Div<SOLineExt.usrTotalProfit, SOLine.curyExtCost>>, decimal100>))]
[PXDefault(TypeCode.Decimal, "0.0")]
Code Extension:
#region Event Handlers
protected void SOLine_UsrGPPercent_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
PX.Objects.SO.SOLineExt soLineExt = PXCache<SOLine>.GetExtension<PX.Objects.SO.SOLineExt>(row);
if (e.ExternalCall)
{
if (soLineExt.UsrGPPercent > 0)
{
if (row.OrderType == "SO")
{
decimal NewUnitPrice;
decimal GPPercent = soLineExt.UsrGPPercent ?? 0;
decimal UnitCost = row.CuryUnitCost ?? 0;
decimal QtyOrdered = row.OrderQty ?? 0;
NewUnitPrice = (UnitCost + ((GPPercent / (decimal)100) * UnitCost));
soLineExt.UsrTotalProfit = (NewUnitPrice * QtyOrdered) - (UnitCost * QtyOrdered);
row.CuryUnitPrice = NewUnitPrice;
row.UnitPrice = NewUnitPrice;
}
}
}
}
#endregion
UsrTotalProfit
Attributes:
[PXDBCurrency(typeof(SOLine.curyInfoID), typeof(SOLineExt.usrTotalProfit))]
[PXUIField(DisplayName = "Total Profit", Enabled = false)]
[PXFormula(typeof(Sub<SOLine.curyLineAmt, SOLine.curyExtCost>))]
[PXDefault(TypeCode.Decimal, "0.0")]

Create new Note on POLine during po creation based on value of custom field

Suppose I have a custom field SOLineExt.UsrCustomField that allows text entry, accessible from SO301000. If I'm creating a PO from PO505000 screen based off this SOLine, I want to automatically create a note on the new POLine and insert the value of SOLineExt.UsrCustomField as the NoteText, but only if SOLineExt.UsrCustomField != null.
The custom field is also accessible as POFixedDemandExt.UsrCustomField (it is populated with fixedDemand IEnumerable override in POCreate), so I can feed it through an override of FillPOLineFromDemand() in POOrderEntry if I need to.
1) do I need to create the new Note in FillPOLineFromDemand() or in POLine_RowInserted(), or somewhere else?
2) what code will create a note and insert the value of UsrCustomField into NoteText? Do I need to create and populate a Note DAC?
You can set a text note using the SetNote static method of PXNoteAttribute class.
There are some unexpected issues when calling that method on a POLine object in the context of FillPOLineFromDemand method. Presumably because the POLine object is not properly initialized in cache at that point.
You can use POLine_RowInserted, I tested this solution:
public void POLine_RowInserted(PXCache sender, PXRowInsertedEventArgs e)
{
POLine row = e.Row as POLine;
POLinkSO.DAC.POLineExt rowExt = row != null ? row.GetExtension<POLinkSO.DAC.POLineExt>() : null;
if (rowExt != null)
{
SOLine line = PXSelectReadonly<SOLine,
Where<SOLine.orderNbr, Equal<Required<SOLine.orderNbr>>,
And<SOLine.orderType, Equal<Required<SOLine.orderType>>,
And<SOLine.lineNbr, Equal<Required<SOLine.lineNbr>>,
And<SOLineExt.usrCustomField, IsNotNull>>>>>.Select(Base,
rowExt.UsrPOLinkSOOrderNbr,
rowExt.UsrPOLinkSOOrderType,
rowExt.UsrPOLinkSOLineNbr);
SOLineExt lineExt = line != null ? line.GetExtension<SOLineExt>() : null;
if (lineExt != null)
{
PXNoteAttribute.SetNote(sender, row, lineExt.UsrCustomField);
}
}
}

BQL on custom DAC referencing SOOrder.orderNbr not reflecting current Order Nbr

We have a test DAC called UsrNonRelatedScanField with two fields : OrderNbr and ScanStatus.
Here's our simple query to grab the correct ordernbr and assign it to a SOOrderExt field:
NonRelatedScanField lastScan = PXSelect<NonRelatedScanField,
Where<NonRelatedScanField.orderNbr,
Equal<Required<SOOrder.orderNbr>>>>.Select(Base, row.OrderNbr);
if(lastScan != null)
{
rowExt.UsrNonRelatedScanField = lastScan.ScanStatus;
}
This logic is held in a SOOrder_RowSelecting() method.
Full Method implementation:
protected virtual void SOOrder_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
SOOrder row = (SOOrder)e.Row;
if (row == null) return;
SOOrderExt rowExt = PXCache<SOOrder>.GetExtension<SOOrderExt>(row);
NonRelatedScanField lastScan = PXSelect<NonRelatedScanField,
Where<NonRelatedScanField.orderNbr,
Equal<Required<SOOrder.orderNbr>>>>.Select(Base, row.OrderNbr);
if (lastScan != null)
{
rowExt.UsrNonRelatedScanField = lastScan.ScanStatus;
}
}
Expected Results : Get the current Orders scan status from lastScan DAC
Actual Results: Will populate correctly only on the initial order opened. When selecting other orders the old value is persisting unless I manually refresh the page. When manually refreshed the correct data comes in.
I haven't had any issues in the past with BQL queries, this specific query is not behaving as expected.
Thank you

Updating ALL SOLine items unit prices dynamically when a new SOLine is added

I have a stored procedure that's called by a PXAction. I know it's against Acumatica's best practices to use a stored procedure, but I have yet find an alternative solution for my goal. The stored procedure evaluates each line item and the price class it's associated with depending on the breakQuantity that determines the unit price. If multiple items belong to the same price class == or exceed the break quantity the unit price is reduced.
What I started with was a row updating
protected virtual void SOLine_RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
{
SOLine row = (SOLine)e.Row;
formalizeOrderTotal(row);
}
then in my formalizeOrderTotal function it performed a foreach loop on SOLine in lines.Select() to add up order quantity. As a test i just tried adding up all order quantities and applying it to every line item. This only updated after refreshing which negates the purpose of moving the stored procedure to a c# function/Acumatica event handler.
If anyone has some recommendations a good approach to updating all line items in cache it would be greatly appreciated if you could provide some input.
Try using Base.Transactions.View.RequestRefresh(); which will ask the grid to refresh itself. In this example, I am setting each line quantity to the number of SOLines present in the grid.
using PX.Data;
namespace PX.Objects.SO
{
public class SOOrderEntry_Extension:PXGraphExtension<SOOrderEntry>
{
protected virtual void SOLine_RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
{
SOLine row = (SOLine)e.Row;
formalizeOrderTotal(row);
}
private void formalizeOrderTotal(SOLine row)
{
foreach (SOLine line in Base.Transactions.Select())
{
if(line.Qty == Base.Transactions.Select().Count)
{
continue;
}
line.Qty = Base.Transactions.Select().Count;
Base.Transactions.Update(line);
Base.Transactions.View.RequestRefresh();
}
}
}
}

Resources