Developer's guide to POS customizations
Development of cross-sell functionality in AX Retail POS is a real-life developer’s story.As I was new to the POS development, I started by googling for some hints. Indeed, there are nice articles describing particular development tasks. However, I was missing a more complex example putting all things together. Especially, after some reading I knew how to create a new form for cross-sell items and how to link it to a new POS operation (which I named CrossUpSell, using the so called BlankOperations). I also learnt that I will have to modify the ItemTriggers assembly to be able to invoke the CrossUpSell operation. However, I did not get any trace of how the invocation can be implemented. And there is more – changes must be done at AX and Commerce Data Exchange too. Therefore, after successfully implementing the up-sell functionality I decided to write down my notes and share them.
The expected audience of this paper are Dynamics AX developers seeking for guidance on performing POS customizations.
Many useful links to information on POS developmentI suppose Dynamics AX 2012 and .NET programming skills and also understanding of the Dynamics AX Retail architecture. Throughout the article, you will find links to numerous sources of information on various aspects of POS development.
Let me start by shortly explaining the desired functionality. In AX, you can define the so called cross-sell items for the main product. These cross-sell items are displayed to a user when a sales line with the main product is created. The user can then make a selection of cross-sell items to be added to the sales order together with the main product. For example, if a PC mouse is being sold then battery pack would be offered. The screenshot below shows the new POS dialog screen that automatically pops up after scanning barcode for product 0003. The user can mark several lines with cross-sell items and after clicking on the OK button new lines will be added to the order.
In AX, cross-sell items are defined in the Up-sell/cross-sell items form which can be opened from the Product information management > Common > Released products by clicking the Up-sell/cross-sell setup button on the Setup tab (up-sell is used for scenarios, when the item replaces the original main product; it is not used in our scenario). In the simplest case the cross-sell definition consists just of one piece of information – the ItemId of the cross-sell item. While there are more parameters for the setup of cross-sell items (Priority, Rules, Scripts), I will not cover them in this article to keep things simpler. For more details on Up-sell/cross-sell functionality, see Technet
Commerce Data Exchange
Before developing customizations at the POS side, the cross-sell setup table must be added to the store database and to the Commerce Data Exchange infrastructure. The following steps are fully explained in the Implementation guide for Commerce Data Exchange PDF document , chapter Customize Synch Service.
In AX, the cross-sell setup data is stored in MCRUpSellItem table. To add this table to the synchronization schema, the following steps must be taken in AX:
- Open the Retail > Setup > Retail scheduler > Retail channel schema form and navigate to the relevant record (e.g. AX 2012 R3).
- Open the Location tables form by clicking on the Location tables button. Add a new record with ax.MCRUPSELLITEM value in the Table name field.
- Open Location fields form by clicking on the Location fields button. Add records for all relevant table columns (at least ITEMID, LINKEDITEM, RECID, UPSELLTYPE). Only fill in the information in the Field name field, go with default values for Type and Length columns (these are not used with this version of the schema).
- Create a new subjob for MCRUpSellItem table. Open the Retail > Setup > Retail scheduler > Scheduler subjobs form and create a new record. Use MCRUpSellItem as a value for both Subjob number and Description fields. For Retail channel schema field, be sure to use the same value where the new Location table was defined (e.g. AX 2012 R3). In Channel table name field use ax.MCRUPSELLITEM and in the Microsoft Dynamics AX table name field use MCRUpSellItem. This maps AX MCRUpSellItem table to the ax.MCRUPSELLITEM channel database table.
- Map fields – open Field list form by clicking on the Transfer field list button. Map fields manually or use the Functions > Match fields button to have AX do the work for you.
- Assign the MCRUpSellItem subjob to the 1040 (Products) job. Open the Retail > Setup > Retail scheduler > Scheduler jobs form and navigate to the 1040 (Products) record. In the Subjobs section, add a new record for MCRUpSellItem subjob.
- Back in the Retail channel schema form, click on the Generate classes button. After the compilation and incremental CIL have finished, the changes will be visible in the RetailCDXChannelSpecificData_AX63 class. It will contain three new methods with MCRUpSellItem in its name.
Now the AX part is finished, we have to add MCRUPSELLITEM table to the channel database’s ax schema. The create SQL script will look like this (the script has been created :
Note that except the table itself we also create indices and grant permissions to DataSyncUsersRole. You might wonder how one comes up to the script implementation – I had a look at the DatabaseScript.txt file within CreateDatabase project in Services solution (which I will talk more at the end of this paper) to see how Microsoft does it.
For this moment, it is pretty OK to run the script from within SQL Server Management Studio during development phase. The proper way of deploying channel database changes is to modify the applications responsible for channel database creation / updates. This is described later in this article.
It is time to test the synchronization now. Make sure there are some records in the Up-sell/cross-sell setup table in AX and launch the synchronization job. Open the Retail > Periodic > Data distribution > Distribution schedule form, navigate to the 1040 (Products) record and click on the Run now button. After confirmation the data transfer is launched. If everything goes OK, cross-sell setup data will appear in the ax.MCRUPSELLITEM table in the corresponding channel database. To troubleshoot issues, start in AX in the Retail > Inquiries > Commerce Data Exchange > Download sessions form. Clicking on the Process status messages button you will update the status in the Status field in the form. Another good place to look for some troubleshooting hints is the Event Viewer both at the Async Server and Async Client environments.
Finally, we need to develop customizations at the POS side. To this end we will already need the Visual Studio and Retail SDK installed, see Technet for details on this.
In the following sections I will show how to:
– Create a new CrossUpSell project containing definition of the new frmCrossUpSell windows which implements the cross-sell dialog window
– Use BlankOperations functionality to open this form
– Launch the BlankOperations operation in reaction to adding an item to the sales order at POS’s main window using triggers
We will start the development in the Services solution which a part of Retail SDK (.\Retail SDK CU9\POS Plug-ins\Services). We will create a brand new project called CrossUpSell. This project will contain most of the source code needed for implementing the cross-sell functionality in POS. I was mostly guided by the existing frmItemSearch form in the WinFormsTouch subfolder of the Dialog project. The resulting frmCrossUpSell has almost 1000 lines of source code. Therefore, I am not going to explain every piece of code here. Instead, I will only point out the important part.
First of all, when creating new CrossUpSell project in Visual Studio (of class library type), do not forget to setup the project itself. Namely, it is wise to set the output path of the project to the ..\..\POS\bin\Debug\ (..\..\POS\bin\Release\) folder to simplify the deployment. Second, as we will add icons to the dialog, we need resources file. To this end, I created a Resources subfolder in the CrossUpSell folder (do it from File Explorer, not from Visual Studio). Then I copied all resource files from Dialog\Resources folder to CrossUpsell\Resources folder. Finally, in Visual Studio drag&drop Resources.resx file from Dialog\Properties folder to CrossUpSell\Properties folder.
As a next step create new class called LineSelection which will hold information about the selected item. The Quantity property is not used throughout this paper; it is for future use only. Also note that the class should be serializable.
Now it is time to add a new Windows Form item to the CrossUpSell project. To keep things as similar to the standard frmItemSearch form as possible I decided to throw away the auto-generated Windows Form designer code and put everything into one frmCrossUpSell.cs file (and thus the corresponding frmCrossUpSell class needs not to be defined as partial).
To keep aligned with the look and feel and common behavior (like exceptions treatment) of the POS application it is important to inherit the form from the frmTouchBase class in the LSRetailPosis.POSProcesses namespace:
At this point, do all the usual windows form development stuff – add all necessary windows forms controls to the form – define private variables for them and create control instances in the private InitializeComponent method called from within the constructor. Moreover, add member variables keeping the form’s status:
The public interface to this form consists of two properties. MainItemId property enables the caller to enter the ItemId of the main product for which cross-sell products should be displayed. The SelectedLines property contains list of selected items that were marked by the user working with the dialog.
Further, override the OnLoad method to initialize the form. Among others, call code for getting initial data for the grid. Namely, call the SearchItems method, which in turn calls the GetItemList method.
Inside the GetItemList method the data is obtained from the channel database to populate the grid control. Its implementation is as follows:
You will notice the use of PRODUCTFINDCROSSUPSELL SQL Server stored procedure in GetItemList method. I created this stored procedure as an analogy to the existing PRODUCTSEARCH stored procedure in the standard Retail Channel Database. It takes the ItemId of the main product (and some other auxiliary parameters) and returns all cross-sell records:
As you can see, internally it calls another stored procedure called GETUPSELLITEMS which does the actual cross-sell item data search:
The rest of the PRODUCTFINDCROSSUPSELL takes care of sorting and rules out the products that are not assorted.
Going back to the frmCrossUpSellItem form, we need a way to communicate the result (the selected cross-sell items) back to the caller. This is done in the SelectItems method which is called when the OK button is pressed:
The method simply enumerates all selected rows (the indices of which are stored in the selectedRowIndices member variable), takes their ItemIds and stores them in the selectedLines member variable which is accessible via SelectedLines public property.
As Enterprise POS is not fully open source solution, we cannot add a new operation for cross-sell directly. Instead, we use the BlankOperations functionality. For introduction to BlankOperations see Technet . There is also a nice article about BlankOperations by Erstad Shane.
To implement BlankOperations in our solution we start by creating a new BlankOperationsCrossUpSell class in the CrossUpSell project. This class must implement the IBlankOperations interface and it must be decorated by the Export attribute. The latter is important for the POS application to identify this class as a plug-in implementing the IBlankOperations interface. For details how this works in .NET, see Managed Extensibility Framework (MEF) articles on MSDN .
The IBlankOperations interface from the Retail SDK declares BlankOperation method, which is implemented in BlankOperationsCrossUpSell type as follows:
Basically, this method only treats blank operations with operation ID equal to CrossUpSell. After some sanity checks, it creates a new instance of our frmCrossUpSell form with MainItemId set to the ItemId obtained from the currently running sales transaction. It then calls the POSFormsManager’s ShowPOSForm method to display the form. After the selection has been made and confirmed in the form, the selected lines are obtained from the form’s SelectedLines public property. The selected lines are then enumerated and added to the current sales transaction by invoking the ItemSale operation with the appropriate ItemId. If everything goes OK, the blank operation is marked as handled – this prevents other implementations of this interface from handling this operation too.
You have probably noticed the lines testing and manipulating the insideCrossUpSell static property of Boolean type.
This is a safeguard that prevents us from displaying the cross-sell dialog window recursively. This has been disabled by design as our customer does not require such a functionality; if cross-sell items for cross-sell items (and further levels) were allowed, then we would have to think of a more user friendly solution, somehow flattening all possible cross-sell products of the root product.
The way how our BlankOperation method uses static property for managing recursive invocation is far from being perfect – at least it is not written in a thread-safe way. It would be a better solution to narrow the scope of the insideCrossUpSell Boolean variable to the sales transaction. It can be done with the use of transaction’s PartnerData property – this technique is described in Erstad Shane’s blog , step 5.
There is one more task in connection with blank operations – we have to modify the standard Retail SDK “implementation” of the IBlankOperations interface which can be found in the BlankOperations class of the BlankOperations project – just make sure that there will be no message box displayed for the CrossUpSell BlankOperations operation. This is important as we cannot predict which of the IBlankOperations implementations will be executed first.
Triggering CrossUpSell blank operation
Now that we have a new dialog form and a new CrossUpSell blank operation, we need to decide when and how to invoke it. In our solution, we do not need to bind a button to the blank operation, as the cross-sell functionality will be triggered by adding a main item line to the sales order. The good place for this is the PostSales method in the ItemTriggers class. This class is located in the ItemTriggers project of the Triggers solution.
The implementation is quite simple, yet a little bit tricky:
The only thing we need to do in the PostSale method is to invoke the CrossUpSell blank operation. To this end, we need an instance of a type implementing the IApplication interface to call its RunOperation method. Here MEF comes into game again – this time we are importing the IApplication interface implementation and store the result into public Application property. This is done automatically when the POS application starts – in other words, during the POS startup the setter is called passing in the actual IApplication implementation instance.
When calling the IApplication’s RunOperation method, we need to pass in the required operation ID (CrossUpSell in our case). This can be done by sending a string parameter consisting of two substrings delimited by semicolon. The first substring represents the operation ID, the second substring represents extra info value. These two substring correspond to the Operation number and Blank operation param fields in the Configure button form in AX, which you can open from the screen layout designer. In our case, we only use the operation ID substring.
Thanks to the MEF support in the standard POS application, it is quite easy to deploy and test the POS changes. Just locate the Extensions subfolder of the POS installation main folder (usually C:\Program Files (x86)\Microsoft Dynamics AX\60\Retail POS, but it is a good idea to make a local copy of the POS installation before deploying any customizations for testing) and place all needed assembly files. Note that you can only debug your customizations if you compile them with the Debug configuration in Visual Studio and deploy the PDB files together with assembly files.
As mentioned earlier, there is a way how to package and deploy channel database upgrade scripts. In Services solution, there is a CreateDatabase project, that contains artifacts for including the scripts. Namely, in the Upgrades subfolder, create a new file (in our case, Upgrade22.214.171.124.sql), that contains all SQL scripts needed for cross-sell functionality. In the POSISUPGRADES.xml, register the new file
and build the CreateDatabase project. Finally, copy the resulting assembly file (CreateDatabase.dll) into two places – the Retail Channel Configuration Utility installation folder (C:\Program Files (x86)\Microsoft Dynamics AX\60\Retail Database Utility\Services) and the Retail Channel Database installation folder (C:\Program Files (x86)\Microsoft Dynamics AX\60\Retail Channel Database\Tools). The former is used by Retail Channel Configuration Utility, which is the standalone tool for creating/upgrading channel databases:
The latter is the tool used by the Dynamics AX installation program when installing the channel database:
Note that neither of the tools supports MEF plug-ins. That’s why it is necessary to replace the original CreateDatabase.dll files (of course, after making a backup of them). For more information on the Retail Channel Database Utility see Technet .
In this article an example of a more complex Enterprise POS customization was given. It comes from a real life scenario – implementation of cross-sell functionality at Enterprise POS was required. The example illustrates all activities needed for its implementation – changes in AX, in Commerce Data Exchange and in POS application. It contains links to the documentation on particular topics as well as troubleshooting hints.
Ondřej Liberda works as a Dynamics AX Developer/Architect in the company Blue Dynamic and has extensive experience with Dynamics AX development.