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.
Related
I have 50+ custom paired fields "Inches" and "Centimeters", each enabled and editable. I need to update "Inches" if the user changed the value of "Centimeters" and visa verse. I was able to do this using SetValuePending on one of the paired fields and SetValueExt on the other during the Field Updated Event. My question, is there a way to do this on a higher level without having to do a Field_Updated event for all the 100+ fields. I know that Formulas would create a circular reference so cannot be used. Thanks
Well, you can use one method to handle FieldUpdated events for all fields you need using graph FieldUpdated.AddHandler method in constructor. To get a field name just extend a standard Acumatica FieldUpdated delegate with one additional parameter (name for example) and put it during the FieldUpdated.AddHandler call.
Here is an example with "Invoices and Memos" screen and ARInvoiceEntry graph.
public ARInvoiceEntry()
{
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.inches).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.inches).Name));
FieldUpdated.AddHandler(typeof(ARTran), typeof(ARTran.centimeters).Name, (sender, e) => CommonFieldUpdated(sender, e, typeof(ARTran.centimeters).Name));
...
}
protected virtual void CommonFieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e, string name)
{
// do something
}
Moreover, you can add handlers dynamically using fields collection for example
foreach(var field in Caches[typeof(ARTran)].Fields)
{
// add handler depends on field name
}
You can do it by PXFormula + ExternalValue BQL expression (the same as PXRowUpdatedEventArgs.ExternalCall for example), which will prevent circular reference between pair fields. The idea is to calculate field only when a related field has been changed by the user from UI (ExternalCall = true) and skip calculation when related field updated by the formula (ExternalCall = false).
public class centimetersInInches : PX.Data.BQL.BqlDecimal.Constant<centimetersInInches>
{
public centimetersInInches() : base(2.54m) { }
}
[PXDecimal]
[PXUIField(DisplayName = "Inches")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Div<centimeters, centimetersInInches>>))]
public virtual decimal? Inches { get; set; }
public abstract class inches : PX.Data.BQL.BqlDecimal.Field<inches> { }
[PXDecimal]
[PXUIField(DisplayName = "Centimeters")]
[PXUnboundDefault(TypeCode.Decimal, "0.0")]
[PXFormula(typeof(ExternalValue<Mult<inches, centimetersInInches>>))]
public virtual decimal? Centimeters { get; set; }
public abstract class centimeters : PX.Data.BQL.BqlDecimal.Field<centimeters> { }
And aspx
<px:PXGridColumn DataField="Inches" CommitChanges="True" />
<px:PXGridColumn DataField="Centimeters" CommitChanges="True" />
You can use the RowUpdated event and compare the old row with the new row to detect which field changed. I agree that keeping all logic in a single method is preferable.
public virtual void DAC_RowUpdated(PXCache sender, PXRowUpdatedEventArgs e)
{
DAC row = e.Row as DAC;
DAC oldRow = e.OldRow as DAC;
if (row == null || oldRow == null) return;
// Compare old row with new row to determine which field changed
if (row.Inches != oldRow.Inches)
{
// Inches field changed, update CM value
row.CM = row.Inches * INCHES_TO_CM_CONSTANT;
}
// Add more conditions for the other fields
[..]
}
I know that Formulas would create a circular reference so cannot be
used.
Yes I wouldn't recommend it either, you could use the DAC property Setter though.
public string _Inches
public virtual string Inches
{
get
{
return this._Inches;
}
set
{
this._Inches = value;
this.CM = value * INCHES_TO_CM_CONSTANT;
}
}
For all solution (except Formula/DAC attributes) I think a condition to stop recursion should be possible if it's absolutely necessary:
if (this.CM != value * INCHES_TO_CM_CONSTANT)
this.CM = value * INCHES_TO_CM_CONSTANT;
Ideally proper use/avoidance of SetValueExt to control when events are raised (Ext method raises events) would be enough to stop infinite loops.
I've been tasked with adding the default vendor inventory ID to grids in multiple screens in Acumatica. The field does not need to be bound and is disabled. I've gotten it as far as showing the field correctly, but only on saved records. Adding a new line and even refreshing the grid will not display the ID, I have to close the screen or switch to another record and come back before the vendor ID will display, even clicking the save button and refreshing will not cause it show. The client is using this field as a reference point so it's important it shows as soon as they select an item.
Below is the code I have for the Kit Specification screen, I need to figure out a way to make it a little more reactive, at least show properly on a refresh. I have tried using Current<> in the where statement, but that just breaks it entirely and always shows nothing.
public class INKitSpecStkDetExt : PXCacheExtension<PX.Objects.IN.INKitSpecStkDet>
{
#region VendorInventoryCode
public abstract class vendorInventoryCode: IBqlField { }
[PXDBScalar(typeof(Search2<PO.POVendorInventory.vendorInventoryID,
InnerJoin<IN.InventoryItem,
On<PO.POVendorInventory.vendorID, Equal<IN.InventoryItem.preferredVendorID>, And<PO.POVendorInventory.inventoryID, Equal<IN.InventoryItem.inventoryID>>>>,
Where<IN.InventoryItem.inventoryID,Equal<IN.INKitSpecStkDet.compInventoryID>>,
OrderBy<Desc<PO.POVendorInventory.vendorInventoryID>>>))]
[PXString(40, IsUnicode = true)]
[PXUIField(DisplayName = "Vendor Inventory Code", Enabled=false)]
public string VendorInventoryCode{ get; set; }
#endregion
}
Once I have the code nailed down it will be used in several other places. Help very much appreciated! Frustrating to have it so close and not be able to cross the finish line...
Follow up based on feedback from HB_Acumatica, working code is below for reference:
public void INKitSpecStkDet_VendorInventoryCode_FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
var row = e.Row as INKitSpecStkDet;
if (e.Row != null)
{
PO.POVendorInventory vendorInventory = PXSelectReadonly2<PO.POVendorInventory,
InnerJoin<IN.InventoryItem,
On<PO.POVendorInventory.vendorID, Equal<IN.InventoryItem.preferredVendorID>, And<PO.POVendorInventory.inventoryID, Equal<IN.InventoryItem.inventoryID>>>>,
Where<IN.InventoryItem.inventoryID, Equal<Required<IN.INKitSpecStkDet.compInventoryID>>>,
OrderBy<Desc<PO.POVendorInventory.vendorInventoryID>>>.Select(Base, row.CompInventoryID);
e.ReturnValue = vendorInventory != null ? vendorInventory.VendorInventoryID : null;
}
}
PXDBScalar attribute doesn't refresh by itself. Maybe explicitly calling RaiseFieldDefaulting method will refresh it:
object newValue = null;
base.Caches[typeof(INKitSpecStkDet)].RaiseFieldDefaulting<INKitSpecStkDetExt.vendorInventoryCode>(rowINKitSpecStkDet, out newValue);
Using PXFormula instead of PXDBScalar if possible will yield better automatic refresh behavior but has it's own set of limitation as well.
If you're looking for the simplest way that works in most contexts (except when no graph is used like in reports and generic inquiry) that would be the FieldSelecting event.
You can execute BQL and return any desired value from the event. It will be called each time the field is referenced so it should update by itself.
public void INKitSpecStkDet_VendorInventoryCode_FieldSelecting(PXCache sender, PXFieldSelectingEventArgs e)
{
PO.POVendorInventory vendorInventory = PXSelectReadonly2<Po.POVendorInventory […]>.Select(Base);
e.ReturnValue = vendorInventory != null ? vendorInventory.VendorInventoryCode : null;
}
We're using the Run Project Billing screen to create records in AR / Invoice and Memo.
In the Invoice & Memo screen, we need the process to populate the header Customer Ord. number, along with a user field that has been added to the grid section on the 'Document Details' tab. At the moment, the process is not doing this.
I'd like to intercept the processing action on the screen using a technique I'm familiar with, namely using an 'AddHandler':
[PXOverride]
protected virtual IEnumerable Items (PXAdapter adapter)
{
PXGraph.InstanceCreated.AddHandler<BillingProcess>((graph) =>
{
graph.RowInserting.AddHandler<BillingProcess.ProjectsList>((sender, e) =>
{
//Custom logic goes here
});
});
return Base.action.Press(adapter);
}
I see no Base.Actions that remotely resembles 'Bill' or 'Bill All'.
This is obviously not exactly the code I need, but I would think this is the general place to start.
After reviewing the source business logic, I don't see any 'Bill' or 'Bill All' Actions - or any 'Actions' at all (baffling). I see an IEnumerable method called 'items', so that's what I started with above.
Is this the correct way to go about this?
Update: 2/14/2017
Using the answer provided re: the overridden method InsertTransaction(...) I've tried to set our ARTran user field (which is required) using the following logic:
PMProject pmproj = PXSelect<PMProject, Where<PMProject.contractID, Equal<Required<PMProject.contractID>>>>.Select(Base, tran.ProjectID);
if (pmproj == null) return;
PMProjectExt pmprojext = PXCache<PMProject>.GetExtension<PMProjectExt>(pmproj);
if (pmprojext == null) return;
ARTranExt tranext = PXCache<ARTran>.GetExtension<ARTranExt>(tran);
if (tranext == null) return;
tranext.UsrContractID = pmprojext.UsrContractID;
Even though this sets the user field to the correct value, it still gives me an error that the required field is empty when the process finishes. My limited knowledge prevents me from understanding why.
On the Run Project Billing screen, captions of Process and Process All buttons were changed to Bill and Bill All respectively in BLC constructor.
Process delegate is set for Items data view within the BillingFilter_RowSelected handler:
public class BillingProcess : PXGraph<BillingProcess>
{
...
public BillingProcess()
{
Items.SetProcessCaption(PM.Messages.ProcBill);
Items.SetProcessAllCaption(PM.Messages.ProcBillAll);
}
...
protected virtual void BillingFilter_RowSelected(PXCache cache, PXRowSelectedEventArgs e)
{
BillingFilter filter = Filter.Current;
Items.SetProcessDelegate<PMBillEngine>(
delegate (PMBillEngine engine, ProjectsList item)
{
if (!engine.Bill(item.ProjectID, filter.InvoiceDate, filter.InvFinPeriodID))
{
throw new PXSetPropertyException(Warnings.NothingToBill, PXErrorLevel.RowWarning);
}
});
}
...
}
As code snippet above confirms, all records in the AR Invoice and Memos screen are created by instance of the PMBillEngine class. Below is code snippet showing how to override InsertNewInvoiceDocument and InsertTransaction methods within the PMBillEngine BLC extension:
public class PMBillEngineExt : PXGraphExtension<PMBillEngine>
{
public delegate ARInvoice InsertNewInvoiceDocumentDel(string finPeriod, string docType, Customer customer,
PMProject project, DateTime billingDate, string docDesc);
[PXOverride]
public ARInvoice InsertNewInvoiceDocument(string finPeriod, string docType, Customer customer, PMProject project,
DateTime billingDate, string docDesc, InsertNewInvoiceDocumentDel del)
{
var result = del(finPeriod, docType, customer, project, billingDate, docDesc);
// custom logic goes here
return result;
}
[PXOverride]
public void InsertTransaction(ARTran tran, string subCD, string note, Guid[] files)
{
// the system will automatically invoke base method prior to the customized one
// custom logic goes here
}
}
Run Project Billing process invokes InsertNewInvoiceDocument method to create new record on the AR Invoice and Memos screen and InsertTransaction method to add new invoice transaction.
One important thing to mention: overridden InsertNewInvoiceDocument and InsertTransaction methods will be invoked when a user launches Run Project Billing operation either from the processing Run Project Billing screen or from the data entry Projects screen.
For more information on how to override virtual BLC methods, see Help -> Customization -> Customizing Business Logic -> Graph -> To Override a Virtual Method available in every Acumatica ERP 6.1 website
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();
}
}
}
}
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();
}