I have a custom graph with multiple grids under different tabs. Each grid has a column for sales order order numbers, which have redirect actions to the SOOrderEntry graph. One of the grids use an action with this code to find and insert the sales order into the redirected graph, and this works fine:
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
graph.Document.Current = graph.Document.Search<SOOrder.orderNbr>(row.OrderNbr);
However, using that same code in an action for a different grid does not work. Stepping through the debugger shows that
graph.Document.Search<SOOrder.orderNbr>(row.OrderNbr);
returns an empty result.
Each action has DependOnGrid=true set in the pages data source, each grid has SyncPosition=true. Even when hardcoding a string order number into the search parameter it returns an empty result.
The workaround I've found to work is to use
PXSelect<SOOrder>.Search<SOOrder.orderNbr, SOOrder.orderType>(graph, row.OrderNbr, row.OrderType);
within the actions that aren't working.
Has anyone experienced this or maybe has some insight into what's going on here? It's strange to me that very similar redirect actions within the same graph need different code to work properly.
Edit: adding the view definitions
This is the view that uses the action with Search to find the needed document.
public PXSelectReadonly2<
SOOrder,
InnerJoin<BAccount,
On<SOOrder.customerID, Equal<BAccount.bAccountID>>,
LeftJoin<STSalesTerritory,
On<BAccountExt.usrTerritoryID, Equal<STSalesTerritory.territoryID>>>>,
Where2<
Where<SOOrder.orderType, Equal<SOOrderTypeConstants.salesOrder>,
And<SOOrder.status, NotEqual<SOOrderStatus.completed>,
And<SOOrder.status, NotEqual<SOOrderStatus.invoiced>,
And<SOOrder.status, NotEqual<SOOrderStatus.cancelled>>>>>,
And<
Where2<
Where<Current<STSalesTerritoryInqFilter.ownerID>, IsNotNull,
And<Current<STSalesTerritoryInqFilter.territoryID>, IsNull,
And<Current<STSalesTerritoryInqFilter.repID>, IsNull,
And<BAccountExt.usrTerritoryID, IsNull,
And<SOOrder.ownerID, Equal<Current<STSalesTerritoryInqFilter.ownerID>>>>>>>,
Or<
Where2<
Where<Current<STSalesTerritoryInqFilter.ownerID>, IsNotNull,
Or<Current<STSalesTerritoryInqFilter.territoryID>, IsNotNull,
Or<Current<STSalesTerritoryInqFilter.repID>, IsNotNull>>>,
And<
Where2<
Where<Current<STSalesTerritoryInqFilter.ownerID>, IsNull,
Or<STSalesTerritory.ownerID, Equal<Current<STSalesTerritoryInqFilter.ownerID>>>>,
And<
Where2<
Where<Current<STSalesTerritoryInqFilter.territoryID>, IsNull,
Or<STSalesTerritory.territoryID, Equal<Current<STSalesTerritoryInqFilter.territoryID>>>>,
And<Where<Current<STSalesTerritoryInqFilter.repID>, IsNull,
Or<STSalesTerritory.repID, Equal<Current<STSalesTerritoryInqFilter.repID>>>>>>>>>>>>>>>
OpenSOOrders;
And this is the view with the action where the search doesnt work, and a PXSelect is needed to find the right document
public PXSelectJoinGroupBy<
Customer,
InnerJoin<Address,
On<Customer.defAddressID, Equal<Address.addressID>>,
InnerJoin<SOOrder,
On<Customer.bAccountID, Equal<SOOrder.customerID>>,
InnerJoin<BAccount,
On<Customer.bAccountID, Equal<BAccount.bAccountID>>,
InnerJoin<STSalesTerritory,
On<BAccountExt.usrTerritoryID, Equal<STSalesTerritory.territoryID>>>
>>>,
Where2<
Where<SOOrder.orderDate, GreaterEqual<Current<STSalesTerritoryInqDateFilter.startDate>>,
And<SOOrder.orderDate, LessEqual<Current<STSalesTerritoryInqDateFilter.endDate>>>>,
And<
Where2<
Where<SOOrder.orderType, Equal<SOOrderTypeConstants.salesOrder>,
Or<SOOrder.orderType, Equal<SOOrderTypeConstantsExt.websiteOrder>>>,
And<
Where2<
Where<SOOrder.status, NotEqual<SOOrderStatus.hold>,
And<SOOrder.status, NotEqual<SOOrderStatus.cancelled>,
And<SOOrder.status, NotEqual<SOOrderStatus.voided>,
And<Customer.customerClassID, NotEqual<marketplaceCustomerClassID>
>>>>,
And<
Where2<
Where<Current<STSalesTerritoryInqFilter.territoryID>, IsNull,
Or<BAccountExt.usrTerritoryID, Equal<Current<STSalesTerritoryInqFilter.territoryID>>>>,
And<Where<Current<STSalesTerritoryInqFilter.ownerID>, IsNull,
Or<STSalesTerritory.ownerID, Equal<Current<STSalesTerritoryInqFilter.ownerID>>>>>>>>>>>>,
Aggregate<
GroupBy<Customer.acctCD>>,
OrderBy<SOOrder.orderDate.Desc>>
NewCustomers;
I am not sure I understand enough of what you are doing to be able to help, but I'll try.
The primary key of SOOrder is CompanyID, OrderType, OrderNbr - that means you should search for SOOrder by OrderType and OrderNbr. You did that in the "workaround" but not the first attempt.
If your grids return values and you just need to make the Order Nbr field a clickable link to the SOOrderEntry screen, add the field to the view (in the screen editor) under the grid's "Levels" section and then mark the OrderNbr field as AllowEdit = true to make it a clickable. Assuming you are showing the OrderNbr in the grid from the SOOrder table, the SOOrder DAC defines the primary graph of SOOrder as SOOrderEntry and will know how to redirect there for you.
If you really need to extract the current SOOrder from the view OpenSOOrders to manually redirect to the order, you access the "Current SOOrder" from the view (made possible by your grid setting "SyncPosition=true") via OpenSOOrders.Current. For example, you can find the current SOOrder, create an instance of the SOOrderEntry graph, and set the current SOOrder (Document) as follows:
SOOrder order = OpenSOOrders.Current;
SOOrderEntry graph = PXGraph.CreateInstance<SOOrderEntry>();
graph.Document.Current = order;
This should work because order is the exact record in the database. To search with just the order type and number, you would use:
graph.Document.Current = graph.Document.Search<SOOrder.orderType, SOOrder.orderNbr>
(order.OrderType, order.OrderNbr);
For completeness, if SOOrder was being updated in a method that you are overriding, you would use Locate instead of Search. Search will find the record in the database, but Locate will find it in the cache. This means that if the method updated values then you will retrieve those updates rather than wiping them out with a fresh copy of the record that Search would retrieve. Locate uses the key fields of the specified record to locate the record in the cache as shown below.
graph.Document.Current = graph.Document.Locate(order);
As for the view not finding the current record for you, it seems you aggregating by Customer. I don't use aggregate often, but I belive this means the results will be returned summarized by customer. If there is more than 1 OrderNbr value, it cannot be shown in the aggregate. As the second view aggregates by Customer, you would need to allow the user to select orders of the customer and use Current from THAT view.
If you want your aggregate in 1 grid and have it drive a view of SOOrders of the selected customer, you would add a new view (and secondary grid) for SOOrder as follows:
public PXSelect<SOOrder,
Where<SOOrder.customerID, Equals<Current<Customer.bAccountID>>>> CustomerOrders;
Just to put a plug in for FBQL, you could write that as:
public SelectFrom<SOOrder>
.Where<SOOrder.customerID.IsEqual<Customer.bAccountID.FromCurrent>>
.View CustomerOrders;
The SyncPosition on NewCustomers should cause CustomerOrders to refresh as you change rows of NewCustomers. Again, on this view, I would make the OrderNbr field of SOOrder a clickable link by using AllowEdit = true.
I have encountered scenarios in pages with 2 primary keys where the Search<> is invoked as follows:
graph.Document.Search<SOOrder.orderNbr>(row.OrderNbr, row.OrderType);
There are multiple references in Acumatica's source code. A good example can be found in the SOInvoiceEntry graph >> RecalcUnbilledTax() method:
soOrderEntry.Document.Current = soOrderEntry.Document.Search<SOOrder.orderNbr>(order.SOOrderNbr, order.SOOrderType);
Your second view has an aggregated selection, grouped by Customer.AcctCD. For the same customer you might have multiple orders: which one should be displayed and opened? By default, Acumatica generates the SQL with MAX for unspecified columns.
I would try to take the aggregation out of the view and see if it works.
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 have a customization that overrides the Purchase Order Line Account field drop-down lookup selector on the Purchase Order screen. It populates like I want, but the account selected from the dropdown doesn't display in the grid field after chosen. Here it is:
[PXSelector(
typeof(Search5<Account.accountID,
InnerJoin<PMCostBudget, On<Account.accountGroupID, Equal<PMCostBudget.accountGroupID>>>,
Where2<Where<PMCostBudget.projectID, Equal<Current<POLine.projectID>>, Or<Current<POLine.projectID>, IsNull>>,
And2<Where<PMCostBudget.projectTaskID, Equal<Current<POLine.taskID>>, Or<Current<POLine.taskID>, IsNull>>,
And2<Where<PMCostBudget.costCodeID, Equal<Current<POLine.costCodeID>>, Or<Current<POLine.costCodeID>, IsNull>>,
And<Where<Current<POLine.lineType>, Equal<POLineType.nonStock>, Or<Current<POLine.lineType>, IsNull>>>>>>,
Aggregate<GroupBy<Account.accountID>>,
OrderBy<Asc<Account.accountCD>>>),
DescriptionField = typeof(Account.description),
Filterable = false,
SelectorMode = PXSelectorMode.DisplayModeValue
)]
It basically filters on the line type, project, task, and cost code selected on the same PO line. What am I missing or doing wrong so that the selected AccountCD value will display?
Assuming the rest of your customization properly handled the override and the selector itself works (I think that is what you are saying)... If you need to display AccountCD, you should add SubstituteKey = typeof(Account.accountCD). Without that, the selector is set right now to display the AccountID.
[PXSelector(
typeof(Search5<Account.accountID,
InnerJoin<PMCostBudget, On<Account.accountGroupID, Equal<PMCostBudget.accountGroupID>>>,
Where2<Where<PMCostBudget.projectID, Equal<Current<POLine.projectID>>, Or<Current<POLine.projectID>, IsNull>>,
And2<Where<PMCostBudget.projectTaskID, Equal<Current<POLine.taskID>>, Or<Current<POLine.taskID>, IsNull>>,
And2<Where<PMCostBudget.costCodeID, Equal<Current<POLine.costCodeID>>, Or<Current<POLine.costCodeID>, IsNull>>,
And<Where<Current<POLine.lineType>, Equal<POLineType.nonStock>, Or<Current<POLine.lineType>, IsNull>>>>>>,
Aggregate<GroupBy<Account.accountID>>,
OrderBy<Asc<Account.accountCD>>>),
SubstituteKey = typeof(Account.accountCD),
DescriptionField = typeof(Account.description),
Filterable = false,
SelectorMode = PXSelectorMode.DisplayModeValue
)]
On PXSelector, the first "typeof" is the value to be selected. You can add subsequent typeof() references if you want to designate fields to display in the PXSelector (if your intent is to display an actual selector).
SubstituteKey = typeof(DAC Field) alters the selector's display to show the designated field rather than the actual value. It is very common to select the recordID field and substitute the recordCD field.
DescriptionField displays the field designated after the displayed valued (the specifically selected field or the SubstituteKey field if specified). I could be wrong, but I don't believe this shows when the selector is displayed in a grid. I believe it only applies to form fields, such as if you toggle the grid row to a form view.
Filterable is optional, but it allows you to set filters in the selector, such as when you have a lot of records to retrieve and may want to quickly locate a value. I believe there is some overhead to using it, so setting to false as you did could be a tiny performance gain.
I never used SelectorMode before, so you taught me something new here! If adding SubstituteKey does not resolve your issue, you might try removing the SelectorMode line to see if that resolves your issue. By the way it reads, that could be changing the behavior of the selector from what I would expect.
Acumatica support's suggestion solved my problem. Since the Account id is a segmented key, I needed to manually change the PXSegmentedValue to a PXSelector in the .ASPX to get it to work. I also got it to work without that change by using a PXDimensionSelector instead of a plain PXSelector in the override.
[PXDimensionSelector(AccountAttribute.DimensionName,
typeof(Search5<Account.accountID>...
I am trying to make a field required on the line item of an AP Invoice, the Tax Category field. However when I change the field to be required I run into the problem of the detail total and the balance to no longer update on the form.
What I've tried doing is eliminating the PersistingCheck = PXPersistingCheck.Nothing of the PXDefault attribute of the TaxCategoryID. This causes the field to be required on the form, however as I've stated, it also causes the form to no longer update totals. I've tried changing the PersistingCheck to PXPersistingCheck.Null, but this also prevents the totals from being updated.
Originally the PXDefault attribute for the Tax Category field is as follows:
[PXDefault(typeof(Search<InventoryItem.taxCategoryID,
Where<InventoryItem.inventoryID, Equal<Current<APTran.inventoryID>>>>),
PersistingCheck = PXPersistingCheck.Nothing)]
This is what my code is:
[PXDefault(typeof(Search<InventoryItem.taxCategoryID,
Where<InventoryItem.inventoryID, Equal<Current<APTran.inventoryID>>>>))]
What I want is to be able to have the Tax Category field required and the totals to be updated as usual, but I am not able to due to something in the code preventing the totals to be updated when the PXDefault attribute of the Tax Category field is changed.
Is there anything additional I must do in order for these issues to be resolved or am possibly going about this the wrong way?
You need to correctly change the PersistenceCheck and add Required=true to PXUIFieldAttribute for showing a red asterisk symbol near the column's name. Please see the example of how to do that using PXMergeAttributesAttribute and PXCustomizeBaseAttribute:
public class APInvoiceEntry_Extension : PXGraphExtension<APInvoiceEntry>
{
#region Event Handlers
[PXMergeAttributes(Method = MergeMethod.Merge)]
[PXCustomizeBaseAttribute(typeof(PXUIFieldAttribute), nameof(PXUIFieldAttribute.Required),true)]
[PXCustomizeBaseAttribute(typeof(PXDefaultAttribute), nameof(PXDefaultAttribute.PersistingCheck), null)]
protected virtual void APTran_TaxCategoryID_CacheAttached(PXCache cache)
{
}
#endregion
}
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.