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

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

Related

How to disable the sales order screen discount calculation

I have a modification in the sales order needs to update the sales order line unit price with a derived value. This is working well and the new unit price shows up after the item has been selected and my code in the SOLine_RowUpdating event has executed. However after the quantity is selected the SOLine_RowUpdating executes again and after that the system calculates discounts like it normally would. Since I have my own price that should not be discounted I'd like to over-ride or cancel this standard discount calculation and just leave my price as is. Here is the SOLine_RowUpdating code and this is working well.
protected virtual void SOLine_RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
{
if (e.NewRow == null) {return; }
Customer customer = Base.customer.Current;
if (customer == null) return;
SOLine soLine = (SOLine)e.NewRow;
int BAAccountID = Convert.ToInt32(customer.BAccountID);
int lCompanyID = PX.Data.Update.PXInstanceHelper.CurrentCompany;
int lInventoryID = Convert.ToInt32(soLine.InventoryID);
LookupPriceAndDiscountDetails(BAAccountID, lCompanyID, lInventoryID); // My own code
sender.SetValueExt<SOLine.curyUnitPrice>(soLine, gdNewUnitPrice); //New price is in gdNewUnitPrice
Base.Transactions.Cache.RaiseRowUpdated(soLine, soLine);
Base.Transactions.View.RequestRefresh();
}
After lots of investigation I found this method suggested by various posts that is supposed to clear out / cancel discounts and in fact I can find it in the standard PX.Objects.SO.SOOrderEntry Row_Updated event but when I try it in my graph extension it does not update or clear out in that the soline (cache) values still show the discount numbers. I must be missing something simple.
Any ideas appreciated at this point...
protected void SOLine_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
SOLine row = e.Row as SOLine;
DiscountEngine<SOLine>.ClearDiscount(sender,row);
// RecalculateDiscounts(sender, row); // ? (Maybe this)
}
You are on right track. Just few more comments.
1. Acumatica will always execute basic SOLine_RowUpdated event and then will execute your's SOLine_RowUpdated
2. If you want to control execution flow of SOLine_RowUpdated events, you can do something like this:
protected void SOLine_RowUpdated(PXCache cache, PXRowUpdatedEventArgs e, PXRowUpdated del)
{
//some code
//del or pointer to basic method can be called like this:
del(cache, e);
}
del will be pointer to method SOLine_RowUpdated
You'll have following options:
a. Don't call del at all ( I don't recommend you this approach because basic method has plenty of staff in it, not just discount calculation)
b. call del and then remove discount information
c. In case if your method ClearDiscount will not clear discount information, then it can be because it's tries to achieve it via simple assign, maybe you can try instead SetValueExt method.
One more point to keep in mind. By default Acumatica will call basic RowUpdated method and then it will call your method, so you don't need to use delegate. I'd recommend you to start from SetValueExt in your RowUpdated graph extension instead of simple assignment.
To make it easier you could just turn off the call to RecalculateDiscounts using the following graph extension:
public class SOOrderEntryExtension : PXGraphExtension<SOOrderEntry>
{
[PXOverride]
public virtual void RecalculateDiscounts(PXCache sender, SOLine line, Action<PXCache, SOLine> del)
{
// if no discounts wanted, just return
// else call the base/standard Acumatica calc discounts on sales order...
if (del != null)
{
del(sender, line);
}
}
}
You can also write your own pricing logic by using a graph extension on ARSalesPriceMaint:
public class ARSalesPriceMaintExtension : PXGraphExtension<ARSalesPriceMaint>
{
[PXOverride]
public virtual decimal? CalculateSalesPriceInt(PXCache sender, string custPriceClass, int? customerID, int? inventoryID, int? siteID, CurrencyInfo currencyinfo, decimal? quantity, string UOM, DateTime date, bool alwaysFromBaseCurrency,
Func<PXCache, string, int?, int?, int?, CurrencyInfo, decimal?, string, DateTime, bool, decimal?> del)
{
//run your custom price logic here and return
// or return the base/standard Acumatica price logic...
return del?.Invoke(sender, custPriceClass, customerID, inventoryID, siteID, currencyinfo, quantity, UOM, date, alwaysFromBaseCurrency);
}
}
This way you do not need to fight the events, but override the calls the events are using to set Discounts and Price on the sales order. I also believe ARSalesPRiceMaint extension will override other screens using the pricing logic which helps to reduce duplicate code on different order entry screens.

Acumatica Unbound User Fields on SOOrder

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

Unbound fields on DAC extension - where to populate?

I have extended the CRMarketingListMember DAC in order to include a number of unbound fields. I'm adding these new unbound fields to the List Members grid on CR204000 and need to execute some code in order to put values into these fields when the List Members grid is displayed. The problem is that my unbound fields are always blank in the grid. I've tried extending the CRMarketingListMaint graph and putting the code that populates the unbound fields into the CRMarketingList_RowSelected() event but that, of course, doesn't work.
Thanks for the help!
RowSelecting is the proper event in which to populate unbound fields, if doing additional BQL selects make sure to wrap the logic in a new PXConnectionScope.
Example Below :
public virtual void ARInvoice_RowSelecting(PXCache sender, PXRowSelectingEventArgs e)
{
ARInvoice row = e.Row as ARInvoice;
if (row != null)
{
using (new PXConnectionScope())
{
ARRegisterExtension rowExt = PXCache<ARRegister>.GetExtension<ARRegisterExtension>(row);
var result = PXSelect<.....>
rowExt.UsrISExternalTax = result.IsExternalTax;
}
}
}

Sales Order SOLines Conditionally Disabled tranDesc being overridden

I have custom code on the Sales Order form that prevents editing SOLine descriptions unless it's a specific SOLine Code. I see that my custom code is being reached, but it seems that something else is overriding my logic, enabling editing on the field after I disable it. I was wondering if there are pre-defined automations that might be doing this for the Sales Order screen, or if there is some other place I should be looking to prevent this behavior.
//I had debug code that validated that RowSelected and SetEnabled were begin called
public class SOOrderEntry_Extension:PXGraphExtension
{
#region Event Handlers
protected void SOLine_RowSelected(PXCache cache, PXRowSelectedEventArgs e, PXRowSelected InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (SOLine)e.Row;
if(row != null && row.InventoryID.HasValue) {
//Only allow editing of parts description if the partno is 'NOTE'
InventoryItem inventoryItem = PXSelect<InventoryItem, Where<InventoryItem.inventoryID, Equal<Required<InventoryItem.inventoryID>>>>.Select(this.Base, row.InventoryID);
if(inventoryItem.InventoryCD == "NOTE") {
PXUIFieldAttribute.SetEnabled<SOLine.tranDesc>(cache, row, true);
} else {
PXUIFieldAttribute.SetEnabled<SOLine.tranDesc>(cache, row, false);
}
}
}
}
Unfortunately, your RowSelected handler will make no effect on the Line Description column because of how automation steps are configured for the Sales Orders screen:
In order to keep Line Description open for editing, you should change a number of Automation Steps disabling the entire Document Details grid on Sales Orders and subscribe to RowSelected handler for the SOOrder DAC to allow editing on the cache level for the SOOrder and SOLine DACs. Below are the changes required to enable Line Description for completed sales orders (if necessary, similar changes should be made to other Automation steps defined for the Sales Orders screen):
Subscribe to SOOrder_RowSelected handler to allow editing on the cache level for the SOOrder and SOLine DACs:
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public void SOOrder_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
SOOrder order = e.Row as SOOrder;
if (order != null && order.Status == SOOrderStatus.Completed)
{
sender.AllowUpdate = true;
Base.Transactions.Cache.AllowUpdate = true;
}
}
}
Modify Sales Orders' SO Completed automation step to open Line Description for editing. In addition to enabled Line Description, it's necessary to enable at least one of the SOOrder fields, otherwise the Save button will never become enabled for completed sales orders:

Tax calculation is wrong when updating UnitPrice while creating the Invoice from Shipment, TaxAttribute.Calculate uses the old extprice

In our add-on, we are modifying the UnitPrice when creating the invoice from the shipment based on our own calculation with a Regex expression. Everything seems fine for all fields, except the taxes who always take the SOOrder taxes.
We created a PXGraphExtension and used the following:
public void InvoiceOrder(DateTime invoiceDate, PXResult<SOOrderShipment, SOOrder, CurrencyInfo, SOAddress, SOContact, SOOrderType> order, PXResultset<SOShipLine, SOLine> details, Customer customer, DocumentList<ARInvoice, SOInvoice> list,
Action<DateTime, PXResult<SOOrderShipment, SOOrder, CurrencyInfo, SOAddress, SOContact, SOOrderType>, PXResultset<SOShipLine, SOLine>, Customer, DocumentList<ARInvoice, SOInvoice>> method)
{
using (var scope = new PXTransactionScope())
{
method(invoiceDate, order, details, customer, list);
LookupBalesFromOrderAndShipment();
scope.Complete();
}
}
In the LookupBalesFromOrderAndShipment(); is where we modify the UnitPrice based on our calculation, and call a Base.Transactions.Update(updatedLine); with our new value and then Base.Persist();
All data seems fine except the Tax Details tab, which still uses the old CuryLineAmt instead of the new CuryLineAmt of the newly created invoice.
I have tried forcing tax recalculation in the RowUpdated event but it doesn't seem to be doing anything.
protected virtual void ARTran_RowUpdated(PXCache cache, PXRowUpdatedEventArgs e, PXRowUpdated InvokeBaseHandler)
{
if (InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
TaxAttribute.Calculate<ARTran.taxCategoryID>(cache, e);
}
Declaring var row = (ARTran)e.Row; and inspecting the variable tells me the right new CuryLineAmt, but that taxes still seem to be calculated on what came in from the SOOrder.
Is there any other way to force a tax calculation or update the ARTax and ARTaxTran table without any risks?
The base function InvoiceOrder changes the tax calculation methods to fit the initial SalesInvoice creation:
TaxAttribute.SetTaxCalc<ARTran.taxCategoryID>(this.Transactions.Cache, null, TaxCalc.ManualCalc);
Every other time you access this page, the tax calculation method used is the one set in SOInvoiceEntry's constructor:
TaxAttribute.SetTaxCalc<ARTran.taxCategoryID>(Transactions.Cache, null, TaxCalc.ManualLineCalc);
Your solution would be to set back the appropriate tax calculation method after calling your base delegate:
public delegate void InvoiceOrderDelegate(DateTime invoiceDate, PXResult<SOOrderShipment,SOOrder,CurrencyInfo,SOAddress,SOContact,SOOrderType> order, PXResultset<SOShipLine,SOLine> details, Customer customer, DocumentList<ARInvoice,SOInvoice> list);
[PXOverride]
public void InvoiceOrder(DateTime invoiceDate, PXResult<SOOrderShipment,SOOrder,CurrencyInfo,SOAddress,SOContact,SOOrderType> order, PXResultset<SOShipLine,SOLine> details, Customer customer, DocumentList<ARInvoice,SOInvoice> list, InvoiceOrderDelegate baseMethod)
{
baseMethod(invoiceDate,order,details,customer,list);
TaxAttribute.SetTaxCalc<ARTran.taxCategoryID>(Base.Transactions.Cache, null, TaxCalc.ManualLineCalc);
var tran = Base.Transactions.Current;
tran.CuryUnitPrice = tran.CuryUnitPrice + 10m;
Base.Transactions.Update(tran);
Base.Actions.PressSave();
}

Resources