Saturday, January 17, 2015

Tutorial: InventDimCtrl_Frm classes and the amazing things you can do with their help

Preamble

I was making some adjustments to one of the forms in the application today. Specifically, it was the PdsCustSellableDays form, which is a setup form you can open for a selected customer through the Sell\Setup\Sellable Days action pane button. You can read more about this functionality here:

Working on these changes brought back the memory of what it is that I have always enjoyed most about developing customizations in AX: Making a lot of difference with only a few small targeted code changes.

So I was inspired to use this as an example for describing some of the capabilities of the InventDimCtrl class hierarchy. There are a lot of classes in the hierarchy, targeted to various forms in the application that display Inventory dimensions one way or the other. The main purpose of this class hierarchy is to control the display and behavior of the inventory dimensions as well as the “Dimension display” button that is normally also present on such forms.

Most of you probably know that the way AX developers early on decided to handle inventory dimensions is through a “reference” table InventDim, and a lot of generically written code that would heavily exercise reflection and traversing form controls. This seemed like a good idea at that time, I bet, but over the years, in my opinion, it got completely out of control, and now is a pretty large overhead for almost all inventory related operations in our product. But, at the same time, it provides a nice level of flexibility to how dimensions are displayed on a particular form.

That being said, let’s start from the beginning and briefly explain the existing form behavior and the things I was set to change in it.

Existing behavior and task

Overview of the existing behavior

  • Only product dimensions are displayed in the main grid
  • The relationship to the products is implemented via a “standard” pattern of TableGroupAll. So you can set up Sellable days for a specific item, an item group, or all items.
  • Viewing Inventory\Dimension display shows the standard dialog with all inventory dimensions. However, storage and tracking dimension check-boxes are disabled for editing.
  • Upon adding a grid row and selecting a released product master, all product dimension values are shown, not just those from the released product variants.
  • Only the product dimensions active for the selected product are enabled for editing. If Item code is set to Group or All, all the dimensions are disabled for editing.

Original version of the form

Task

  • Storage and tracking dimensions cannot be selected in Dimensions Display, so should not be shown at all
  • Lookup for configuration and other product dimensions should only show those relevant for the released product master.

 

Explanation

OK, so let’s take a closer look at how such types of forms are built

  • A form-level variable of type InventDimCtrl_Frm is declared. This object is used to control the behavior of the inventory dimension controls on the form.
  • A form method called inventDimSetupObject simply returns the above reference. Through this method other AX forms and classes can access the current state of the form when it comes to the dimensions
    • This includes information about which inventory dimensions are visible in the grid, which of them are enabled for editing, which of them should be marked as mandatory, which of them should be shown when the Dimension Display button is pressed, whether or not the “large” version of the dialog is to be shown (that includes On-hand information like Closed/ClosedQty), whether Item number should be shown on the Dimension Display dialog, etc.You can browse the methods on this class to get a feeling for what else it can do.
    • Since we can only select product dimensions in the form, we want spread that over to the Dimension Display dialog as well.
  • A form method called updateDesign() exists, with a certain pattern for handling various “events” that happen on the form. The main purpose here is to update the inventDimSetupObject with the latest information from the form, and through it refresh the display of the inventory dimension controls.
    • In the PdsCustSellableDays form this happens when the form is opened (Init), whenever the item code or reference is changed (FieldChange), whenever the active record is changed (Active) or whenever the parent customer record is changed in turn refreshing the Sellable Days view (LinkActive).
      • This method is then called from the appropriate event handles or the form, as you can see if you open the form code.
    • In PdsCustSellableDays the intention is to only enable the active product dimensions for the selected item, and disable all of them if we selected all items or an item group instead. But only product dimensions are to be shown ever. This is achieved by using the following methods:
      • parmDimParmEnabled(InventDimParm) controls which dimensions can be edited
      • parmDimParmVisible(InventDimParm) controls which dimensions are shown
      • parmDimParmVisibleGrid(InventDimParm) controls which dimensions are shown in the grid.
      • parmDimParmLockedRightClick(InventDimParm) controls which dimensions can be selected through the Dimension Display button
      • formSetControls(boolean) actually executes the update of the inventory dimension controls based on the above settings. The first parameter controls whether the form is locked for the duration of the update (preventing unnecessary flicker, especially in cases where controls are being made visible/invisible in the grid).
      • formActiveSetup(InventDimGroupSetup), which is not used here, but is frequent in other forms, should be called when the active record is changed, and resets the enabled fields based on the now selected item dimension group information.
      • dimFieldsActive(InventDimParm) that is also used in the form is actually an inquiry-type method, and does not impact the state of the object in any way. Its purpose is to return a container of active dimension field IDs based on an InventDimParm buffer.
  • Lookups for all the product dimensions are based on the form called InventProductDimensionLookup, which is another example of generic handling of inventory dimensions, and has all the above properties as well.
    • It can show up to 3 tab pages, including the selected product dimension values, all relevant product variants and the on-hand information
    • In the current version of the form, all existing product dimension values are shown, regardless of the product. This is one of the things we need to fix.

 

Changes made and result

OK, so in order to accomplish the 2 tasks we set for ourselves above, we only need to change code in 3 places on the form. Let’s walk through each one in turn.

  • Modify the classDeclaration, and replace the type InventDimCtrl_Frm with InventDimCtrl_Frm_ProductDim. If we take a look at this class (it is only available starting from AX 2012 R3 CU8, so I am also including the code for this class below, for reference), we can see a few methods were overridden:
    • mustEnableField() makes sure that only the active product dimensions based on dimension group setup are enabled. This is used by the formActiveSetup method, and is not as interesting for our example.
    • mustShowGridField() makes sure that all product dimensions are shown in the grid, and only product dimensions. This was controlled by explicitly calling parmDimParmVisibleGrid() before. We’ll remove that code, as you’ll see later.
    • setupShowAllProductDimensions() – this method is important for our example. This method, if it returns true, will ensure only product dimensions are shown on the Dimension Display dialog.
  • Modify the updateDesign method as below
    • Remove the lock/unlock method calls, since this is already going to happen within formSetControls().
    • Initialize the inventDimSetupObject using the right class InventDimCtrl_Frm_ProductDim
    • Remove the variables inventDimParmDefault and inventDimParmDisenabled, and all code related to them. This code is now redundant.
    • Remove the call to dimFieldsActive. As I mentioned above, it does not do anything useful for us.
    • We can also add 1 extra line clearing the inventDim values that do not apply – this is important for when changing the item number where we already have product dimensions selected. Just a bug fix, not really related to this tutorial.
  • Add a new method itemId(), which returns the currently selected item number
    • This is necessary for the product dimension lookups to function properly. The lookup tries to find the item information from the calling form through the data source joined to InventDim as a parent, assuming it has an ItemId field. While that is true in most cases, our form uses a TableGroupAll pattern, so the field is a bit different. By creating an itemId() method on the form, we ensure that the lookup can find the right item number to reference when retrieving product dimension group information.
    • In updateDesign, after initializing the object, invoke parmSkipOnHandLookUp(true), which will ensure that the on-hand tab page is not shown on the lookups. Since this is not necessary for this type of setup form, it’s a good idea, since it will improve the lookup loading time. This is also demonstrating how the lookup form uses the inventDimSetupObject of the calling form to get the expected behavior settings.
    • The lookup will now also show the Combinations tab page, if the selected product has more than 1 product dimension active.

That’s it. With very few changes, we were able to drastically change the look and feel of this form. Here’s how it looks now:

Updated form behavior

 

Download the project

You can download the project with the changes I have made from my OneDrive.

Saturday, December 20, 2014

Tutorial: Auto-Complete functionality on form controls


I guess all of you are familiar with the Auto-Complete feature present in Dynamics AX. Whenever you type in a repeated value in a control on AX forms, an automatic suggestion pops up next to the entered characters and the user can choose to accept the suggestion (for example, by tabbing out of the control).
There are methods on the form (specifically, the FormRun class) that provide you with a more tight control over the auto-completion list available to the user, and this blog post will describe the behavior of these methods, as there are a few not obvious things there.

Method signature Method behavior description
getAutoCompleteString(int _elementIndex) Retrieve the auto-complete string saved under the specified index in the auto-completion list of the form.
The indexing is not control-specific, and is not very efficient at keeping the number of values in the list low, so you will see values from multiple controls here.

The method returns a container containing 2 values:
- the auto-completion string value
- the index of this string in the auto-completion list
getAutoCompleteString(int _startIndex, FormControl _control, str _searchStr) Retrieve the auto-complete string for the specified control, finding the first one in the list starting from the specified startIndex + 1.

If _searchStr is also specified, it will find the first matching entry in the list with index > startIndex. This is the closest behavior to what happens when a user types in a certain character in the entry control on the form and the kernel needs to search and populate the auto-complete value for it.

Note: There’s a small bug in this function, where it does not return the full string value for longer texts. A workaround is to use the above “overload” and retrieve the full value by index.
setAutoCompleteString(string _string, anytype _control) The first argument is clear, and that is the auto-completion string to add to the list of suggestions.

The second argument is presented as _control, but can accept either an instance of a FormControl, or an integer value of the list item index to set.

If the _string already exists in the auto-completion list for this control, nothing will happen. If it does not exist, a new item will be added to the list (number of items cannot exceed 300) with the next available index.
setAutoCompleteString(string _string, anytype _index) The auto-completion list item at index _index will be updated in the list with the specified _string value.

This method “overload” basically allows you to override a particular value in the list with a new value.
delAutoCompleteString() Deletes all auto-completion items from the list, regardless of which control they are bound to, if any.
delAutoCompleteString(anytype _index) Deletes the item with the specified index from the auto-completion list.
delAutoCompleteString(anytype _control) Deletes all auto-completion items for the specified control from the list.

When a value in one of the controls on the form is modified, this value is also automatically added to the auto-completion list. With the above methods, however, you can control, if this value should actually stay there, or be immediately removed.
This way you can, for example, restrict the user to only be provided with auto-completion hints from a certain list (similar to IntelliSense).

I created a simple form to demonstrate all the above methods in action.

Tutorial auto-complete capabilities form
Tutorial form to demo auto-completion capabilities

You can download the form from my OneDrive.


Apart from the above methods, there is also a global setting which allows you to control auto-completion list availability for a particular user. This is control on the User options screen, as highlighted in the below image:

Autocomplete in User options
User options form


If this check box is not set, auto-completion suggestions will not be shown to the user when entering data on forms, even though the auto-completion list is still maintained behind the scenes.

Wednesday, December 03, 2014

Tip: Connecting the Warehouse management device portal to a different company or partition in Dynamics AX

As a follow up to my blog post about fixing an annoying error with the warehouse mobile device portal (see original post here), I got a number of people asking the following question:
"How do I install/configure the mobile portal to work in more than one company/partition?"
This is possible, and relatively easy, even though it might not be the most intuitive thing to do. Here's a quick walk-through of allowing work users to access another company account portal.

The Warehouse mobile device portal (WMDP) as of today only supports connecting to a single company account in a single partition. This is driven by the Active directory account used as the identity for the IIS application pool that is used to run the WMDP.

Which means that in order to access another company, we need to:

  • Create another Active directory account
  • Install another instance of the WMDP
    • Use a different port number than the ones already installed
    • Use the above AD user as the identity for the app pool
  • Add a user in AX for the above Active directory account
    • Do it in the partition you want to connect to
    • Set the default company to the one you want to connect to
    • Don't forget to assign the correct security role
    • Hint: You can also set the default user language, which will impact the WMDP site as well
  • Create all the necessary Warehouse management data in the new partition/company. That includes the Display settings, Warehouse workers and mobile device users, warehouses, etc.

Note, that you should avoid having the same AD user (used for WMDP) be part of multiple partitions. Only the first partition (alphabetically) will be connected to, but it becomes confusing for people looking at the data.


Once these steps are complete, you should be able to use the new link (with the new port) to connect to the mobile device portal and log in as a work user from the new company account.

Monday, October 13, 2014

Tutorial: High level overview of entity statuses in the new Warehouse management solution

I’ve seen a number of questions on the community and yammer groups asking about the statuses for Loads, Work, etc, where people were not sure what that means. I dug up this document with a brief description of all the possible statuses for Loads, shipments, Waves and Work in the new solution.

Hope you guys find it helpful, as I did at some point.

Load
  • Open: Load has been created, but no other warehousing processes have been executed on the load. Some or all load lines are not part of a shipment.
  • Posted: Load has been released to warehouse; related load lines have been assigned to shipments. These shipments may or may not be on a wave. If on a wave, the wave has not been executed.
  • Waved: Shipments tied to the load have been assigned to a wave, and the wave was executed. Any work that was created through the waving process has not been started.
  • In Process: Work tied to the load has started being executed, but does not meet the qualifications for an “In Packing” or “Loaded” status (see below).
  • In Packing: Work has been created for the entire quantity of load lines. All work has been put down to the manual pack station.
  • Loaded: Work has been created for the entire quantity of load lines. If not using a packing flow, all work has been put to a final shipping “baydoor” location. If using packing, all containers have been closed (All quantity tied to the load is picked against the appropriate sales/transfer orders).
  • Shipped: Load has been ship confirmed.
  • Received: For inbound loads, product receipt has been run for all quantity tied to the load.
Shipment (similar to load)
  • Open: Shipment has been created, but no other warehousing processes have been executed on the shipment.
  • Waved: Shipment has been assigned to a wave, and the wave has been executed. Any work that was created through the waving process has not been started.
  • In Process: Work tied to the shipment has started being executed, but does not meet the qualifications for an “In Packing” or “Loaded” status (see below).
  • In Packing: Work has been created for the entire quantity of load lines associated with shipment. All work has been put down to the manual pack station.
  • Loaded: Work has been created for the entire quantity of load lines associated with shipment. If not using a packing flow, all work has been put to a “baydoor” location. If using packing, all containers have been closed (All quantity tied to the load is picked against the appropriate sales/transfer orders).
  • Shipped: Shipment has been ship confirmed.
  • Received: For inbound shipments, product receipt has been run for all quantity tied to the shipment.
Wave
  • Created: Wave has been created. The wave may or may not have shipments assigned to it, but it has not yet been executed.
  • Executing: The wave is in the process of being executed. (Useful status for long running waves)
  • Held: The wave has finished executing, but has yet to be released. Any work that was created during the execution process is frozen, preventing it from being started until the wave is released.
  • Released: Wave has been released. Any work tied to the wave is unfrozen and able to be executed.
  • Picked: All work tied to a wave has been completed.
Work (Applies to both work header records and work detail lines)
  • Open: Work or work line has been created, but not started by a worker.
  • In Process: Worker has started work or work line, but not completed either.
  • Pending Review: Specific to cycle count work header only. Occurs when worker counts incorrect quantity or dimensions, but does not have the rights to automatically adjust inventory.
  • Skipped: Applies to work lines only. Status is applied when worker uses the skip option on the RF to cycle through the execution of work detail lines.
  • Closed: Work or work line has been completed.
  • Cancelled: Work or work line has been cancelled.
  • Combined: Applies to work header only. Occurs when executing the “Pick & Pack” RF flow which combines multiple work units into a single one.

Monday, September 22, 2014

Tutorial: Update product receipts for loads and how to handle items missing from ASN

In my post about the ability of receiving a mixed pallet through the License plate receiving method on the mobile device, I mentioned that once the receiving and put away is complete, the next step would be to update the product receipt document. With that, the purchase order status is updated to Received and all relevant GL postings are done.

Posting a product receipt for a load can be done from two places in the new warehouse management module:
  • For a selected load, whether that is from the All loads list page, Load details or from the Load planning workbench. This will execute the posting right away
  • For 1 or more loads, running as a batch job in the background, using the periodic operation "Update product receipts"
The actual business flow is more or less the same regardless of where you launch the update from, and I am going to describe it below, also doing a small demonstration of the forms and data that are relevant.

One thing that I specifically want to call out in this blog post is a piece of functionality related to receiving ASN documents, namely the receiving exceptions for items missing from ASN.

Preparation

So, i guess it's clear that the first step for us to look into the product receipt process is to get a load created and corresponding work created/executed for it. This is described in my post about importing an ASN and using License plate receiving to register their arrival and complete the put-away, so I will start here by just briefly listing the data I am going to work with in this demo:
  • A load with 2 purchase orders on it, with shipping carrier specified as JB Hunt:
    • PO #1 with 1 line for item 000148_202, qty = 2 PL (150 ea), WH = 42
    • PO #2 with 1 line for item 000147_202, qty = 1 PL (100 ea), WH = 42
  • A packing structure for this load (based on ASN received from the vendor)
    • 1 pallet with License plate LP2POs006
      • 75 ea of 000148_202
      • 100 ea of 000147_202
    • 1 pallet with License plate LPMissASN001
      • 75 ea of 000148_202
  • Work is created and executed for LP2POs006 through License plate receiving
    • LPMissASN001 has not been received, as it did not actually arrive on the truck.
What the last line essentially means is that the vendor planned to and believes he sent us the entire order (thus it is in the ASN document), but in reality, one of the pallets is missing. So somebody needs to get notified about this exceptional situation and act on it, ensuring we do not get billed for the missing goods.

In order to showcase the receiving exception related to items missing from load as compared to the ASN I have also specified the corresponding work exception code in the Warehouse management parameters, as shown below:

Warehouse management parameters

Flow description

The "Update product receipts" menu item is located under Warehouse management \ Periodic, and opens into a dialog, where you can select the loads you want to post product receipts for. The criteria could be, for example, the shipping carrier bringing in the loads. If you know that "JB Hunt" delivers before lunch, we can try posting product receipts for their deliveries at, say, 2 pm.

Note

The batch job flow is per my understanding going to be used more often, so this is what I am going to describe in this post. Manual posting is pretty much identical, with 1 difference mentioned below.


Once you confirm the processing, the job will be added to the queue on the batch server, and will be executed based on the recurrence you have selected on the Batch tab page.
For this demo I have not enabled it to run in the batch, so the execution happens right away.

Here's an attempt at capturing what is going to happen with the selected loads when you schedule the product receipt update for them:

  • Loop through all the selected loads
  • If Load has not yet been Received
    • check if the load has been ship confirmed, or the state of the related route (TMS functionality) if a load is part of a route. If it is not confirmed
      • ship confirm the load and all related loads that need to be confirmed at the same time (because they are on the same route)
        • if at least one of these loads cannot be ship confirmed, none of the loads will be confirmed. A number of things are checked here, but they are part of the transportation management flow, which is not the focus of this post, so I will omit describing them
      • As a result, the status of the load changes to Shipped 
    • check the document status of the purchase orders related to the load
      • if the purchase order is not confirmed, confirm it.
    • This entire step is executed within 1 transaction, while the below steps are part of another transaction. So if you for example cancel the actual product receipt, your loads would stay ship confirmed.
  • Loop through the load lines
    • Compare the load line to the related ASN item lines, finding discrepancies between the "planned" receiving quantity on the load line and the quantity work was actually created for based on the ASN packing structure. To rephrase, we compare, how much we ask the vendor to deliver in the selected load and how much he actually delivered.
      • If a discrepancy is found, create a receiving work exception with exception code selected in Warehouse management parameters \ Code for missing items from ASN
Note: We currently do not record the actual quantity discrepancies, just that there was a difference, as well as the load and order line information
    • For each purchase order linked, prepare the order for posting product receipt. All load lines will be posted as part of 1 large update, so if multiple orders exist they will be posted in 1 go, with the same ParmId.
      • Actually post the product receipt for all relevant orders. 
        • Display the Posting product receipt dialog. Note the product receipt IDs are generated as _#, so you should be able to later identify all product receipt documents posted for a load based on such a condition (if you do not override the number with the actual vendor IDs)
        • Select to update only the Registered quantity, as that is what we have received according to the warehouse
        • As a result, the status of corresponding purchase orders is updated to Received, or stays Open if some of the quantity was not received in full. 
      • Update the load, removing load lines that were not received at all and decreasing quantity on those that were only received partially.
  • Update load status to Received
    • Update status of all shipments that are part of the load to Received

Manual posting difference

When posting the product receipt manually, the button to post is actually disabled until the purchase orders in the load are all in Confirmed status.

Demonstration

Navigate to Warehouse management \ Periodic \ Update product receipts, and configure the criteria for updating all loads delivered by JB Hunt that have not been Received yet. 

Update product receipt selection criteria
When you confirm the update, the above flow is going to execute, confirming the inbound shipment and displaying the product receipt posting dialog with both purchases shown. Notice the product receipt numbers assigned, and that only the registered quantity is updated. Confirm the processing.

Product receipt posting dialog
Once the update is complete, we can go back and check on the load. As you can see, the Load Status has changed to Received, and the quantity on the first load line has been decreased to match what we actually received.

Received load information
At the same time a receiving work exception has been logged in the system, pointing to a discrepancy of type "Missing item from ASN", as shown below:

Work exceptions log
If you now take a look at the purchase orders from the All Purchase orders list page, you will see that one of them was fully updated and thus has a status of Received, while the other is still Open and has an expected delivery for the quantity that was missing from the ASN. We'll now need to contact the vendor/carrier and figure out what went wrong with this shipment.

Youtube video walkthrough

As an experiment, I have also recorded the above flow in a screen cast and uploaded it to my Youtube channel. Take a look and let me know what you think. Is that helpful at all? (I take any kind of feedback very well :))

Summary

This post was trying to address two main goals:
  • Explain and demonstrate, what happens behind the scenes when posting a product receipt for loads
  • Show, how you can set up a receiving exception, that will allow more easily tracking cases where something went wrong and we did not get the goods we were supposed to.
I hope it's more clear now and you'll be able to use AX to the full extent when it comes to inbound ASNs.