I noticed that when the quantity of a SOLine is zero, there are no SOLineSplit or INItemPlan records available for that line. The second the quantity is greater than 0, the system makes those records, and if the qty is set back to 0, the records are deleted.
Is there a way to prevent the SOLineSplit and INItemPlan objects from deleting when a record is set to 0 quantity?
Is there a way to still have the system create an SOLineSplit and INItemPlan if the SOLine is initially created with a 0 quantity?
The reason for the question is that a customer wants the system to lock the SOLine after a certain point, but also allow for the Qty to be adjusted from another screen. Since this is not directly changing the value on the SOLine screen, this isn't triggering the events to create the split and plan.
I have tried creating an instance of SOOrderEntry in the custom screen as follows:
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
//Also tried graph.Transactions.Current = line, but did not work
graph.Transactions.Update(line);
graph.Actions.PressSave();
Doing that keeps resulting in a null object reference error:
Error: An error occurred during processing of the field OrderQty : Object reference not set to an instance of an object..
System.NullReferenceException: Object reference not set to an instance of an object.
at PX.Objects.SO.SOOrderEntry.SOLine_OrderQty_FieldUpdated(PXCache sender, PXFieldUpdatedEventArgs e) ...
I think the problem with your statement in using SOOrderEntry is you are not truly loading the order in the graph to update it correctly. After you create the graph instance and before the transaction update, you should load the document header like this...
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
graph.Document.Current = graph.Document.Search<SOOrder.orderNbr>(line.OrderNbr, line.OrderType);
if(graph.Document.Current == null)
{
return;
}
graph.Transactions.Update(line);
graph.Actions.PressSave();
As for controlling how the plan and split records are entered from SOOrderEntry... The entries are controlled through the attributes on PlanID. The cache attached is where this gets added on SOOrderEntry...
[PXMergeAttributes(Method = MergeMethod.Append)]
[SOLineSplitPlanID(typeof(SOOrder.noteID), typeof(SOOrder.hold), typeof(SOOrder.orderDate))]
protected virtual void SOLineSplit_PlanID_CacheAttached(PXCache sender)
{
}
You can make your own graph extension of Sales order and replace the attribute with your own version of SOLineSplitPlanID... it might be a battle for you as I am not sure why you would want the plan record to exist when zero qty to plan.
Related
I have a process that creates records in the Sales Orders screen's Details grid based on two Header user fields: SiteID (Warehouse) and LocationID.
When the 'Create Shipment' process is initiated, the shipment is created which contains the SiteID in the Sales Orders grid - but since there is no locationID in the grid, this 'Create Shipment' process uses some default(?) LocationID, where I'd like to use the Header User field's LocationID.
My question is, how would I intercept this process to set the LocationID to something other than what it's defaulting to?
Thanks...
Update:
Using the virtual method:
SetShipmentFieldsFromOrder(SOOrder order, SOShipment shipment, Nullable<Int32> siteID, Nullable<DateTime> shipDate, String operation, SOOrderTypeOperation orderOperation, Boolean newlyCreated, SetShipmentFieldsFromOrderDelegate baseMethod)
I don't see any way to set the grid value for LocationID (i.e., there is no SOShipLine record to set a value in the virtual method. How would I do this?
There's a virtual method on the SOShipmentEntry graph called SetShipmentFieldsFromOrder, you can override that to update the CustomerLocationID as needed. The create shipment action calls SOShipmentEntry.CreateShipment which inserts the shipment and then calls the SetShipmentFieldsFromOrder method.
The system should be pulling the SOShipment.CustomerLocationID from the SOOrder.CustomerLocationID field by default though.
I believe the question is about defaulting of warehouse locations into the shipment lines and allocations rather than customer locations
Currently, shipment line selects location(s) by the following way
Originally (SelectLocationStatus), it selects location based on their pick priority (smaller value means higher priority)
After this method, the ResortStockForShipmentByDefaultItemLocation is executed. This method puts the default issue location for the item-warehouse combination (InItemSite) at the top of this list regardless of its pick priority.
I believe you should override this method to put the needed location to the top of the list instead of (or ahead of) the default issue location. Here is the code of this method of the SOShipmentEntry class for reference:
protected virtual void ResortStockForShipmentByDefaultItemLocation(SOShipLine newline, List<PXResult> resultset)
if (INSite.PK.Find(this, newline.SiteID)?.UseItemDefaultLocationForPicking != true)
return;
var dfltShipLocationID = INItemSite.PK.Find(this, newline.InventoryID, newline.SiteID)?.DfltShipLocationID;
if (dfltShipLocationID == null)
return;
var listOrderedByDfltShipLocationID = resultset.OrderByDescending(
r => PXResult.Unwrap<INLocation>(r).LocationID == dfltShipLocationID).ToList();
resultset.Clear();
resultset.AddRange(listOrderedByDfltShipLocationID);
}
Important! If we are talking about 21R2 version, there is the "Project-Specific Inventory" (materialManagement) feature which has its own extension of the SOShipmentEntry where some of the shipment creation methods (including the SelectLocationStatus) are overridden. The ResortStockForShipmentByDefaultItemLocation is not overridden, but if the customer uses this feature, I suggest to extend this extension rather than base SOSHipmentEntry:
namespace PX.Objects.PM.MaterialManagement
{
public class SOShipmentEntryMaterialExt : PXGraphExtension<SOShipmentEntry>
I'm new to Acumatica, could you please help me? I have too screens IN202500 (stock items) and SO301000(sales orders). I added a field to stock items and now I need to show a value from that field in grid column of sale orders for each stock items. I suppose that I need to use PXDefault attribute for this?
There are a number of ways you can do this. I'll provide 3 possibilities.
If your View used by the grid contains InventoryItem, you may be able simply to select your custom field from InventoryItem and add it directly to the screen. I'll assume this is not an option or you likely would have found it already.
Create a custom field in a DAC extension on SOLine where you add your custom field as unbound (PXString, not PXDBString) and then use PXDBScalar or PXFormula to populate it. I haven't used PXDBScalar or PXFormula to retrieve a value from a DAC Extension, so I'll leave it to you to research. I do know this is super easy if you were pulling a value directly from InventoryItem, so worth doing the research.
Create as an unbound field as in #2, but populate it in the SOLine_RowSelecting event. This is similar to JvD's suggestion, but I'd go with RowSelecting because it is the point where the cache data is being built. RowSelected should be reserved, in general, for controlling the UI experience once the record is already in the cache. Keep in mind that this will require using a new PXConnectionScope, as Acuminator will advise and help you add. (Shown in example.) In a pinch, this is how I would do it if I don't have time to sort out the generally simpler solution provided as option 2.
Code for Option 3:
#region SOLine_RowSelecting
protected virtual void _(Events.RowSelecting<SOLine> e)
{
SOLine row = (SOLine)e.Row;
if (row == null)
{
return;
}
using (new PXConnectionScope())
{
SOLineExt rowExt = row.GetExtension<SOLineExt>();
InventoryItem item = SelectFrom<InventoryItem>
.Where<InventoryItem.inventoryID.IsEqual<#P.AsInt>>
.View.Select(Base, row.InventoryID);
InventoryItemExt itemExt = item.GetExtension<InventoryItemExt>();
rowExt.UsrSSMyDatAField = itemExt.UsrSSMyDataField;
}
}
#endregion
I am trying to override the unit price in sales order entry with my own price looked up elsewhere. I do have it working with the code below but what I cannot figure out is why the line that is commented out causes a "Stack Overflow". The price does update correctly and does save to the database from session to session.
The examples I followed were provided in this post and there it's clear that the commented out line is needed : Sales Price Updating Every Other Time
What it seems to be doing is the line Base.Transactions.Update(soLine); calls SOLine_RowUpdating and then you in an infinite loop.
Thanks
protected virtual void SOLine_RowUpdating(PXCache sender, PXRowUpdatingEventArgs e)
{
if (e.NewRow == null)
{
return;
}
SOLine soLine = (SOLine)e.NewRow;
decimal NewUnitPrice = 12349.56M;
sender.SetValueExt<SOLine.curyUnitPrice>(soLine, NewUnitPrice);
Base.Transactions.Cache.RaiseRowUpdated(soLine, soLine);
// Base.Transactions.Update(soLine);
Base.Transactions.View.RequestRefresh();
}
Let's say that your extension is named SOOrderEntryExt ( for simplification of explanation ).
Then we have 3 Actors:
1. SOOrderEntry, with it's method SOLine_RowUpdating ( created by Acumatica team )
2. SOOrderEntryExt with it's method SOLine_RowUpdating ( created by you )
3. Acumatica cache
4. Acumatica
Acumatica has following code:
Each time after SOLine is modified in cache, I need to execute two methods:
SOOrderEntry.SOLine_RowUpdating
SOOrderEntryExt.SOLine_RowUpdating
Following line of code:
Base.Transactions.Update(soLine);
basically says to Acumatica, hey, soLine was updated in the cache. What Acumatica should do? Execute those two lines again:
SOOrderEntry.SOLine_RowUpdating
SOOrderEntryExt.SOLine_RowUpdating
and again, and again, and again.
Then question arises, how to execute
SOOrderEntry.SOLine_RowUpdating
without causing eternal cycle? Next line of code is a solution:
Base.Transactions.Cache.RaiseRowUpdated(soLine, soLine);
We have a business requirement to set the SO return COST to the original cost issued without invoicing if possible. We determined that Sales Orders are necessary to track issuing materials to our client, and we are cost driven rather than price driven. We use FIFO costing, but SO return orders do not seem to return at the original COST unless invoiced (which we also don't do in a traditional manner).
I found that setting the unit/ext cost on the SO Shipment Line directly in the database before Confirm Shipment and Update IN appears to provide the results desired. Applying a custom menu option to streamline and strongly control the return, I cloned nearby code as a base. The section between the === is where I set the unit/ext cost. The PXTrace shows the expected value, but it is coming out as $0 on the shipment record. I thought I might need "docgraph.Update(sOShipmentLine)" to save it, but that's not accessible in this scope.
using (var ts = new PXTransactionScope())
{
PXTimeStampScope.SetRecordComesFirst(typeof(SOOrder), true);
//Reminder - SOShipmentEntry docgraph = PXGraph.CreateInstance<SOShipmentEntry>();
docgraph.CreateShipment(order, SiteID, filter.ShipDate, adapter.MassProcess, SOOperation.Receipt, created, adapter.QuickProcessFlow);
PXTrace.WriteError("Setting Cost");
//Set Cost on Shipment to Cost On SO Line
PXResultset<SOShipment> results =
PXSelectJoin<SOShipment,
InnerJoin <SOShipLine, On<SOShipLine.shipmentNbr, Equal<SOShipment.shipmentNbr>>,
InnerJoin <SOLine, On<SOLine.orderType, Equal<SOShipLine.origOrderType>,
And<SOLine.orderNbr, Equal<SOShipLine.origOrderNbr>, And<SOLine.lineNbr, Equal<SOShipLine.origLineNbr>>>>
>>,
Where<SOShipment.shipmentNbr, Equal<Required<SOShipment.shipmentNbr>>>>
.Select(docgraph, docgraph.Document.Current.ShipmentNbr);
PXTrace.WriteError("Shipment {0} - Records {1}", docgraph.Document.Current.ShipmentNbr, results.Count);
foreach (PXResult<SOShipment, SOShipLine, SOLine> record in results)
{
SOShipment shipment = (SOShipment)record;
SOShipLine shipmentLine = (SOShipLine)record;
SOLine sOLine = (SOLine)record;
==============================================
shipmentLine.UnitCost = GetReturnUnitCost(sOLine.OrigOrderType, sOLine.OrigOrderNbr, sOLine.OrigLineNbr, sOLine.CuryInfoID);
shipmentLine.ExtCost = shipmentLine.Qty * shipmentLine.UnitCost;
PXTrace.WriteError(string.Format("{0} {1}-{2} = {3} / {4}", shipmentLine.LineType, shipmentLine.ShipmentNbr, shipmentLine.LineNbr, shipmentLine.Qty, shipmentLine.UnitCost));
==============================================
}
PXAutomation.CompleteSimple(docgraph.Document.View);
var items = new List<object> { order };
PXAutomation.RemovePersisted(docgraph, typeof(SOOrder), items);
PXAutomation.RemoveProcessing(docgraph, typeof(SOOrder), items);
ts.Complete();
}
Still on the learning curve so I'm expecting the solution is likely simple and obvious to someone more experienced.
There's three phase to it:
Changing the value
Updating the cache
Persisting the cache
I think you are changing the value but not persisting it. The reason why it works after invoking Confirm Shipment or Update IN action is probably that these actions will persist all changes by calling the graph Save action.
To change a field value in a data view you would do:
DACRecord.Field = value;
DataView.Update(DACRecord);
The particularity of your example is that the request is not bound to a data view.
When you have a loose BQL request you can do the same operation with a cache object. In your example the Caches context is available from docGraph:
DACRecord.Field = value;
graph.Caches[typeof(DACType)].Update(DACRecord);
graph.Caches[typeof(DACType)].Persist(DACRecord, PXDBOperation.Update);
Update and Persist are often omitted because in many scenarios they will be called later on by other framework mechanism. For example if you were to do only Update on a UI field, the record won't be persisted until the user clicks on the save button.
Updating value on UI is a bit different than updating in cache.
The recommended approach for UI fields is to use SetValue:
cache.SetValue<DAC.DacField>(DACRecord, fieldValue);
Or use SetValueExt when you want to trigger the framework events like FieldUpdated when changing the field value:
cache.SetValueExt<DAC.DacField>(DACRecord, fieldValue);
You'll still have to update and persist the changes in cache for these too if you want the changes to stick without requiring the user to manually save the document.
Below is my code to insert whatever value is entered into my UsrWLAmt field into my BudgetGrid representing the history of the fields values.
I want to raise a warning prompting the user to enter a value into the details field in the BudgetGrid History
protected void PMProject_UsrWLAmt_FieldUpdated(PXCache cache, PXFieldUpdatedEventArgs e, PXFieldUpdated InvokeBaseHandler)
{
if(InvokeBaseHandler != null)
InvokeBaseHandler(cache, e);
var row = (PMProject)e.Row;
PMProject con = Base.Project.Current;
PX.Objects.PM.ProjectExt item = con.GetExtension<PX.Objects.PM.ProjectExt>();
if (item.UsrWLAmt > 0)
{
atcBudgetHis bud = new atcBudgetHis();
bud.CreatedDateTime = DateTime.Now;
bud.Value = item.UsrWLAmt;
BudgetGrid.Insert(bud);
// to attach the exception object to the field
BudgetGrid.View.Cache.RaiseExceptionHandling<atcBudgetHis.details>(
bud, " ",
new PXSetPropertyException(
"Please specifiy reason for budget change.",
PXErrorLevel.Warning));
}
}
I've also tried BudgetGrid.Cahce.RaiseExceptionHandling
The code above doesn't raise any trace errors.
EDIT:
PXUIFieldAttribute.SetWarning<atcBudgetHis.details>(BudgetGrid.Cache, null, "Please specifiy reason for budget change.");
Works for all rows but
PXUIFieldAttribute.SetWarning<atcBudgetHis.details>(BudgetGrid.Cache, bud, "Please specifiy reason for budget change.");
Doesn't raise any warnings.
I could create another field above the grid for the notes to be inserted, but is there a way I can set the warning for the last row in the BudgetGird?
First things first, to show a warning in Acumatica one of the following events must be used:
FieldVerifying and throw PXSetPropertyException, when warning should appear only during the time user updates a record
RowUpdating with RaiseExceptionHandling method invoked on PXCache, if warning should appear on multiple fields only during the time user updates a record
RowSelected with RaiseExceptionHandling method invoked on PXCache, if warning should appear on multiple fields all the time until a user addresses the cause of warning
I guess for your particular scenario, RowSelected might work best to constantly show warnings for all empty cells within Notes column:
public void atcBudgetHis_RowSelected(PXCache sender, PXRowSelectedEventArgs e)
{
atcBudgetHis row = e.Row as atcBudgetHis;
if (row == null) return;
if (string.IsNullOrEmpty(row.Details))
{
sender.RaiseExceptionHandling<atcBudgetHis.details>(row, string.Empty,
new PXSetPropertyException("Please specify reason for budget change.", PXErrorLevel.Warning));
}
else
{
sender.RaiseExceptionHandling<atcBudgetHis.details>(row, row.Details, null);
}
}
It seems like you tried to set a warning on a DAC instance that didn't exists in the grid at the moment the event was called.
Have you tried setting the warning on the existing row returned in the event handler parameter instead?
PXUIFieldAttribute.SetWarning<atcBudgetHis.details>(BudgetGrid.Cache, row, "Please specify reason for budget change.");
The warning applies to all rows that satisfy the condition that executes this line. If you want to display it for only the last row, you would have to manually check if the row received in the parameter is the same as the last row in your data view and only then execute the warning for that row.
The solution was to use the RowInserted event for my grid and pass the row variable into SetWarning
You need to change this code:
BudgetGrid.Insert(bud);
// to attach the exception object to the field
BudgetGrid.View.Cache.RaiseExceptionHandling<atcBudgetHis.details>(bud, " ",new PXSetPropertyException("Please specifiy reason for budget change.",PXErrorLevel.Warning));
To something like this:
bud = BudgetGrid.Insert(bud); //you need to get the "bud" which is in the cache
// to attach the exception object to the field
BudgetGrid.View.Cache.RaiseExceptionHandling<atcBudgetHis.details>(bud, " ",new PXSetPropertyException("Please specifiy reason for budget change.",PXErrorLevel.Warning));