How can extended cost be locked, so that it cannot be keyed over? Currently, a qty and a unit cost calculate, but the extended cost can be changed.
View of Changed Cost
Create a graph extension for POOrderEntry (I am assuming this is the PO Order Screen)
Add a cache attached handler as follows:
[PXUIField(DisplayName = "Ext. Cost", Enabled = false)]
[PXMergeAttributes(Method = MergeMethod.Merge)]
protected virtual void _(Events.CacheAttached<POLine.curyExtCost> e)
{
}
Related
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.
I am trying to make the PriceClassID required for Business Accounts when they are created. I initially did this by editing the DAC. This caused an issue where whenever an Employee was created, an error was displayed making creating an employee impossible.
Error: 'CPriceClassID' cannot be empty
I went back to the drawing board and decided to edit the attributes on the Graph which allowed me to create Employee records. However now when editing existing Vendors via the Business Accounts screen I get the same error. I can create and edit Vendors from the Vendors screen because it uses a different graph but I would still like to implement a more elegant solution
[PXDBString(10, IsUnicode = true)]
[PXSelector(typeof(AR.ARPriceClass.priceClassID))]
[PXUIField(DisplayName = "Price Class", Visibility = PXUIVisibility.Visible)]
[PXDefault()]
protected virtual void Location_CPriceClassID_CacheAttached(PXCache sender)
{
}
What is the best method to make the CPriceClassID field required on the Business Accounts screen that will still allow me to create Employees and Vendors without any errors?
You can use PXUIRequiredAttribute for achieving what you need.
Below is an example of how you can use it for making the field required only on the specific screen:
public class LocationExt : PXCacheExtension<PX.Objects.CR.Location>
{
public class BusinessAccountMaintScreen :Constant<string>
{
//"CR.30.30.00" is the page id of the Business Accounts screen
public BusinessAccountMaintScreen():base("CR.30.30.00")
{
}
}
#region UsrCustomField
[PXDBString(10, IsUnicode = true)]
[PXSelector(typeof(AR.ARPriceClass.priceClassID))]
[PXUIField(DisplayName = "Price Class", Visibility = PXUIVisibility.Visible)]
[PXDefault]
// Here we add checking for the current page so that this field will be required only on the Business Accounts screen
[PXUIRequired(typeof(Where<Current<AccessInfo.screenID>, Equal<BusinessAccountMaintScreen>>))]
public virtual String CPriceClassID {get;set;}
#endregion
}
I'm trying to display a warning every time the ShippedQty field is changed to a value < OrigOrderQty on the "Shipment - SO302000" screen, but I only want the code to be active for that specific screen/form.
I added the code below to extend the SOShipmentEntry graph, which accomplishes my original goal, but the issue is that now the code I added is also being used by the "Create Shipment" action in "Sales Orders - SO301000" screen/form.
Create Shipment Action Discussed
namespace PX.Objects.SO
{
public class SOShipmentEntry_Extension : PXGraphExtension<SOShipmentEntry>
{
#region Event Handlers
protected void SOShipLine_ShippedQty_FieldUpdated(PXCache cache,PXFieldUpdatedEventArgs e)
{
var myrow = (SOShipLine)e.Row;
if (myrow == null) return;
if (myrow.ShippedQty >= myrow.OrigOrderQty)
{
}
else
{
throw new PXSetPropertyException("The difference between the shipped-qty and the ordered-qty will be placed on a back-order", PXErrorLevel.Warning);
}
}
#endregion
}
}
While the warning allows the user to save changes to a shipment on the Shipment Screen/form SO302000 (Because the exception is set up as a warning and not an error), I get the following error when I create a shipment using the "Create Shipment" button on the "Sales Orders - SO301000" screen.
Warning works fine for form-mode
Warning becomes error when processed in background by action button
Any ideas to accomplish this? It is my understanding that if I want to make global changes to a field I must make them in the DAC, but if I want to make changes that only affect screens/forms where a graph is used, then I have to make those changes in the graph code itself. I'm guessing the "Create Shipment" action button in the Sales Orders screen is creating an instance of the graph where I added the code, so I'm wondering what are my options here.
Best regards,
-An Acumatica newbie
If you want your event logic to execute only when CreateShipment is invoked from the Shipment screen you can override the other calls to CreateShipment to dynamically remove your event handler.
The event that invokes CreateShipment action from the SalesOrderEntry graph is Action:
public PXAction<SOOrder> action;
[PXUIField(DisplayName = "Actions", MapEnableRights = PXCacheRights.Select)]
[PXButton]
protected virtual IEnumerable Action(PXAdapter adapter,
[PXInt]
[PXIntList(new int[] { 1, 2, 3, 4, 5 }, new string[] { "Create Shipment", "Apply Assignment Rules", "Create Invoice", "Post Invoice to IN", "Create Purchase Order" })]
int? actionID,
[PXDate]
DateTime? shipDate,
[PXSelector(typeof(INSite.siteCD))]
string siteCD,
[SOOperation.List]
string operation,
[PXString()]
string ActionName
)
In that method it creates a SOShipmentEntry graph to create the shipment. You can override Action and remove your handler from that graph instance:
SOShipmentEntry docgraph = PXGraph.CreateInstance<SOShipmentEntry>();
// >> Remove event handler
SOShipmentEntry_Extension docgraphExt = docgraph.GetExtension<SOShipmentEntry_Extension>();
docgraph.FieldUpdated.RemoveHandler<SOShipLine.shippedQuantity>(docgrapExt.SOShipLine_ShippedQty_FieldUpdated);
// << Remove event handler
docgraph.CreateShipment(order, SiteID, filter.ShipDate, adapter.MassProcess, operation, created);
Note that in order to reference SOShipLine_ShippedQty_FieldUpdated method from another graph you'll have to make it public:
public void SOShipLine_ShippedQty_FieldUpdated(PXCache cache,PXFieldUpdatedEventArgs e)
I have described how to do this in that answer too:
Updating custom field is ending into infinite loop
If you want your event logic to execute only when it is modified in the UI or by web service.
You can use the ExternalCall Boolean property of the PXFieldUpdatedEventArgs parameter.
This property value will be true only when the sender field is modified in the UI or by web service.
Usage example:
protected void SOShipLine_ShippedQty_FieldUpdated(PXCache cache,PXFieldUpdatedEventArgs e)
{
// If ShippedQty was updated in the UI or by a web service call
if (e.ExternalCall)
{
// The logic here won't be executed when CreateShipment is invoked
}
}
ExternalCall Property (PXFieldUpdatedEventArgs)
Gets the value specifying whether the new value of the current DAC field has been changed in the UI or through the Web Service API.
When a Sales Order is created via a REST API call applicable Discounts are not applied automatically like they are when creating the same SO via the standard Acumatica screen. The Discounts should apply and are not "Manual" -- running the "Recalculate Prices" action after being created via the API will apply the Discounts.
Do the Discount Codes have to be specified in the API call or is there some way to get them to be automatically applied like on the screen? Orders received via the API are given a unique Order Type so we could reasonably do a customization to the SOOrderEntry graph for these particular order types that when the SO is initially created to run the "Recalculate Prices" but I couldn't find a way to successfully trigger the Base Action either.
Currently, discounts are completely skipped for all Sales Orders and AR Invoices created or updated via Web API or Import Scenarios. To launch discounts calculation through Web API, you should proceed as follows:
Create a hidden action in the SOOrderEntry BLC extension to recalculate discounts for the current order:
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry>
{
public PXAction<SOOrder> RecalculateDiscountsFromImport;
[PXButton]
[PXUIField(DisplayName = "Recalculate Discounts",
MapEnableRights = PXCacheRights.Select,
MapViewRights = PXCacheRights.Select,
Visible = false)]
public void recalculateDiscountsFromImport()
{
foreach (SOLine line in Base.Transactions.Select())
{
SOLine oldLine = Base.Transactions.Cache.CreateCopy(line) as SOLine;
DiscountEngine<SOLine>.RecalculateDiscountsLine<SOOrderDiscountDetail>(
Base.Transactions.Cache,
Base.Transactions,
line,
line,
Base.DiscountDetails,
Base.Document.Current.BranchID,
Base.Document.Current.CustomerLocationID,
Base.Document.Current.OrderDate.Value);
TX.TaxAttribute.Calculate<SOLine.taxCategoryID>(
Base.Transactions.Cache,
new PXRowUpdatedEventArgs(line, oldLine, false));
}
Base.Transactions.Cache.IsDirty = true;
}
}
Add mapping for the custom action above in an extended endpoint and invoke the action via the Contract API in a separate request after Sales Order was created and saved in Acumatica.
Based on RuslanDev's answer and a couple changes this was the action implemented and then executed in a subsequent API call:
public PXAction<SOOrder> RecalculateDiscountsFromImport;
[PXButton]
[PXUIField(DisplayName = "Recalculate Discounts",
MapEnableRights = PXCacheRights.Select,
MapViewRights = PXCacheRights.Select,
Visible = false)]
public void recalculateDiscountsFromImport()
{
foreach (SOLine line in Base.Transactions.Select())
{
DiscountEngine<SOLine>.RecalculateDiscountsLine<SOOrderDiscountDetail>(
Base.Transactions.Cache,
Base.Transactions,
line,
line,
Base.DiscountDetails,
Base.Document.Current.BranchID,
Base.Document.Current.CustomerLocationID,
Base.Document.Current.OrderDate.Value);
Base.Transactions.Update(line);
Base.Transactions.SetValueExt<SOLine.manualDisc>(line, false);
}
Base.Actions.PressSave();
}
So I added the action to create new Sales Order with the current BAccount.BAccountID and I'm receiving this error "Value cannot be Null. Parameter name:key". Can anyone see specifically what I'm doing incorrectly? I was under the assumption that customerID and BAccountID were equivalent since they held the same values for IDs.
public PXAction<BAccount> KSSOOrderPush;
[PXUIField(DisplayName = "Create New Sales Order", MapEnableRights = PXCacheRights.Select, MapViewRights = PXCacheRights.Select)]
[PXButton(CommitChanges = true)]
protected virtual void kSSOOrderPush()
{
//Must use Base.BAccount.Current INSTEAD of BAccount.Current
BAccount bacct = Base.BAccount.Current;
if (bacct == null || bacct.BAccountID == null) return;
//Create instance of graph
SO.SOOrderEntry graph = PXGraph.CreateInstance<SO.SOOrderEntry>();
graph.Document.Current = graph.Document.Search<SOOrder.customerID>(bacct.BAccountID);
throw new PXRedirectRequiredException(graph, "Sales Order");
}
First of all, you're right - SOOrder.CustomerID takes the same values as BAccount.BAccountID. There is one important note here though: Business Accounts represent not only customers but also vendors, employees, etc., while it is possible to create Sales Orders (as well as AR documents) only for customers - keep this in mind and be sure to check that you're dealing with a customer when in this action (there is a type field on the BAccount class - checking it will be enough).
The problem with your code is that you don't actually create a Sales Order for the selected customer, but try to find one that belongs to this customer and navigate to it.
To create the order you should change the following line
graph.Document.Current = graph.Document.Search<SOOrder.customerID(bacct.BAccountID);
to
// insert an SOOrder with default type
SOOrder newOrder = graph.Document.Insert();
// set appropriate CustomerID and update the order
newOrder.CustomerID = bacct.BAccountID;
graph.Document.Update(newOrder);
With these modifications PXRedirectionException should open the Sales Orders screen with Customer set to the one that you were seeing at the Business Accounts screen.