Tuesday, May 21, 2019

[Development tutorial] Sample test + tips for using ATL (Acceptance Test Library) to implement tests in Warehouse management

Hopefully, by now everyone is familiar with the fact that we shipped our internal test framework for all ISVs, partners and customers to use.
We have published pretty detailed documentation about the framework on docs.microsoft.com:

https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/perf-test/acceptance-test-library

I, however, learn much better when looking at samples. We have shipped a couple sample tests in each area with the 10.0.3 release.
In this post, I would like to share another sample test case and walk through the different types of commands possible in ATL, focusing also on the part that drives mobile device flow operations.

Important note

ATL is not meant as a replacement for good quality production code, and should not be used to execute business logic in PROD environments

Sample test

This test will validate a very simple inbound flow where I want to validate I can confirm the putaway location by the location check digit:
  • Create a new purchase order for a WHS-enabled item to a WHS-enabled warehouse
  • Perform the registration of the item through the mobile device
  • Perform the putaway through the mobile device
    • Confirm the location confirmation is shown and is asking for the check digit
    • Enter the check digit of the putaway location instead of the location ID
    • Complete the putaway
  • Validate the work lines are updated to Closed (not really the point of the check digit test, but just to show the capability)

[SysTestMethod]
public void confirmLocationWithCheckDigit_SimplePurchOrderOneItem_Success()
{
 ttsbegin;

 //========Given========

 purchaseOrder.addLine().setItem(item).setInventDims([warehouse]).setQuantity(PurchaseQuantity).save();

 rf.login().purchaseOrderItemReceiving().setPurchaseOrder(purchaseOrder).setItem(item).setQuantity(PurchaseQuantity).confirmUnitSelection();

 var work = purchaseOrder.work().singleEntity();

 //========When========

 var poPutaway = rf.login().purchaseOrderPutaway().setWork(work).confirmPick();

 poPutaway.parmCheckDigitControl().assertIsEnabled(); // Will automatically happen within set method below
 poPutaway.setCheckDigit(checkDigit); // This will confirm the Put to this location

 //========Then========

 poPutaway.assertWorkCompleted();

 work.reread();
 this.assertEquals(WHSWorkStatus::Closed, work.parmWorkStatus(), 'Work should be closed');

 work.lines().assertExpectedLines(
  whs.workLines().spec().withLineNum(1).withItem(item).withStatusClosed().withQuantity(PurchaseQuantity).withLocation(whs.locations().receipt()),
  whs.workLines().spec().withLineNum(2).withItem(item).withStatusClosed().withQuantity(PurchaseQuantity).withLocation(putawayLocation));

 ttsabort;
}

Looks pretty simple, doesn't it? And does not really require comments, as the code is pretty self explanatory for the most part, "even a functional consultant can read it" :)
A number of things I would like to call out here:
  • Notice the name of the test method. We always try to name the method according to the Given/When/Then pattern (also explicitly called out in the method body), more specifically, When_Given_Then(). Or, for those more familiar with AAA, Arrange_Act_Assert()
    • Note the method name does not start with test. You can still do that, but we prefer being explicit through decorating the method with the SysTestMethod attribute
  • I skipped the test setup part - I'll post that below. Normally that would include steps generic for all tests, while the Given part in here would only contains a few things special to this particular test. In this test, we 
    • create a purchase order line for a small quantity of a WHS-enabled item
    • do the PO receipt through the mobile device, to create the putaway work.
  • There is A LOT of defaulting that happens in these fluent calls. When writing code, you need to pay attention and override any default value. For example, 
    • When setting inventory dimensions for the PO line, I only specify the warehouse, but that in turn sets the Site as part of the business logic. The Inventory status is also defaulted based on the Warehouse parameters. 
    • When logging it to the mobile device in rf.login(), the default worker is used. However you can overwrite that and specify another worker to log in with. 
  • The WMA flows should look like you'd have it on the actual device, with scanner actually sending a caret return aka clicking OK after entry. Note, that defaulting is also supported here, where we just proceed without explicitly specifying a unit
    • Here, however, I used a dedicated method, confirmUnitSelection(), to improve readability. Inside, in the implementation, it's a simple OK button click. 
    • It is possible to override this behavior, by disabling the automatic submission on the flow class. This way, you'll have the ability to for example set multiple controls at the same time before OK'ing, etc. 
  • ATL allows for most areas to rely on either a special entity class, or the underlying record. Generally speaking, we always prefer to use the entity over the record. For example, take a look at how I retrieve the work entity for the putaway work created after registration.
  • Process Guide flows are tested with a different set of classes at this point. We'd be interested to hear your feedback on, which approach is more convenient. 
  • When doing putaway, I also use a "dummy" method, confirmPick() to confirm the pick up of the LP from the RECV location. These methods, however, could be doing some additional validation
    • We have, for example, implemented some like confirmPutToBaydoor(), which will confirm we are in fact in a Put step, and the Location control has a value where it is of type "Final shipping location". Think through these scenarios when extending ATL
  • We can also access the controls shown on the current screen right now, to perform additional validation, for example. I showcase that with the CheckDigit control, simply checking that it is enabled for data entry.
    • This already happens automatically in the next call to setCheckDigit(), so you should consider where you really need this.
    • assertIsEnabled() will also ensure the control is in fact visible.
  • Lastly, I wanted to show how specs are used in a real example, where we in assertExpectedLines() specify the 2 lines we expect to see on the putaway work after execution of the putaway. This allows for really flexible validation of the data, that also is easy to read and understand.
  • Check out how the flow class shows up in the debugger - it displays the XML with the controls, which really helps understand where you are in the flow if something in the test goes very wrong!

Entire test class

Below is the full listing of the xpp class with the test, including variable declarations and setup.
You can also download it from my OneDrive

[SysTestGranularity(SysTestGranularity::Integration),
SysTestCaseAutomaticNumberSequences]
public final class DEV_PurchaseOrderReceivingAndPutawayScenarioTests extends SysTestCase
{
    AtlDataRootNode                     data;
    AtlDataInvent                       invent;
    AtlDataInventOnHand                 onHand;
    AtlDataWHS                          whs;
    AtlDataWHSMobilePortal              rf;
    AtlDataPurch                        purch;

    InventTable                         item;
    InventLocation                      warehouse;
    WMSLocation                         putawayLocation;
    AtlEntityPurchaseOrder              purchaseOrder;

    private const WMSCheckText          CheckDigit = '12345';
    private const PurchQty              PurchaseQuantity = 1;

    public void setUpTestCase()
    {
        super();

        data = AtlDataRootNode::construct();
        invent = data.invent();
        onHand = data.invent().onHand();
        whs = data.whs();
        rf = whs.rf();
        purch = data.purch();

        ttsbegin;

        whs.helpers().setupBasicPrerequisites();
        AtlWHSWorkUserContext::currentWorkUser(whs.workUsers().default());

        item = invent.items().whs();
        warehouse = whs.warehouses().whs();
        whs.warehouses().ensureDefaultReceiptLocation(warehouse);

        putawayLocation = whs.locations().floor();
        putawayLocation.checkText = CheckDigit;
        putawayLocation.update();

        whs.locationDirectives().purchasePutaway();
        whs.workTemplates().purchaseReceipt();

        var poPutawayMenuItem = rf.menuItems().purchaseOrderPutaway();
        rf.menuItems().setupWorkConfirmation(poPutAwayMenuItem.MenuItemName, WHSWorkType::Put, NoYes::No, NoYes::Yes);

        rf.menus().default().addMenuItem(rf.menuItems().purchaseOrderItemReceiving())
                            .addMenuItem(poPutawayMenuItem);

        purchaseOrder = data.purch().purchaseOrders().createDefault();

        ttscommit;
    }

 [SysTestMethod]
 public void confirmLocationWithCheckDigit_SimplePurchOrderOneItem_Success()
 {
  ttsbegin;

  //========Given========

  purchaseOrder.addLine().setItem(item).setInventDims([warehouse]).setQuantity(PurchaseQuantity).save();

  rf.login().purchaseOrderItemReceiving().setPurchaseOrder(purchaseOrder).setItem(item).setQuantity(PurchaseQuantity).confirmUnitSelection();

  var work = purchaseOrder.work().singleEntity();

  //========When========

  var poPutaway = rf.login().purchaseOrderPutaway().setWork(work).confirmPick();

  poPutaway.parmCheckDigitControl().assertIsEnabled(); // Will automatically happen within set method below
  poPutaway.setCheckDigit(checkDigit); // This will confirm the Put to this location

  //========Then========

  poPutaway.assertWorkCompleted();

  work.reread();
  this.assertEquals(WHSWorkStatus::Closed, work.parmWorkStatus(), 'Work should be closed');

  work.lines().assertExpectedLines(
   whs.workLines().spec().withLineNum(1).withItem(item).withStatusClosed().withQuantity(PurchaseQuantity).withLocation(whs.locations().receipt()),
   whs.workLines().spec().withLineNum(2).withItem(item).withStatusClosed().withQuantity(PurchaseQuantity).withLocation(putawayLocation));

  ttsabort;
 }

}

A few extra notes:

  • We recommend that you create a base test class for your test automation, that will hide the ATL navigation variable declaration and initialization, along with potentially some common setup, like I have at the beginning of the setupTestCase() method, setupBasicPrerequisites() and worker initialization.
  • Note the use of SysTestCaseAutomaticNumberSequences attribute on the class. This allows not to worry about configuring them manually, just let the framework do it for you.
  • Note that my test will not use an existing partition and company. It should generally work in USMF as well though, and it's up to you to decide how much data setup you want to rely on.

OK, that's it for this post. It's a bit of a brain dump, but please read through the points, and don't hesitate to reach out with questions.

Hope this helps

Monday, May 06, 2019

Announcement: MDCC is looking for Dynamics 365 Finance and Operations engineers

I'm really happy to share that we've updated the description of open positions (Example: https://careers.microsoft.com/us/en/job/616931/Software-Engineer) to really reflect the focus of the team here in Lyngby, Denmark.

And we couldn't be more serious about finding the right talent to help us build an even better product within Supply Chain Management on the Microsoft Dynamics 365 Finance and Operations platform.

So, regardless of your level of experience (in the positive direction), if you love AX and want to make it better, if you are passionate about business problems, warehousing, manufacturing or sales, and if you are great at and enjoy solving complex technical challenges, apply today for one of the open positions we have!

Thursday, March 28, 2019

[Heads up] Explicit dependency required for enabling Process Guide based flows in Warehouse Mobile App

Introduction

With release of version 8.1.3 (build numbers going as 8.1.227.xxxx), we introduced an additional configuration option on the Mobile device menu items, which controls whether or not the system will attempt using the Process Guide framework for executing a mobile flow
This is a temporary configuration option, and is supposed to serve two purposes:

  • Allow having a partially implemented Process Guide flow, where the customer will be able to decide if the scenarios the current implementation supports are sufficient to transition over, and get to experience the better performance, and be able to rely on the new flow with any tweaks and customizations necessary.
  • Allow to run both Process Guide and WHSWorkExecuteDisplay version of the flow side by side, for testing purposes (and as a risk mitigation in case the new flow has a blocking bug)
There is one small BUT, however.

Detection

For any flows that were implemented before version 8.1.3, where the customer is already live and has multiple mobile device menu items created that are relying on Process Guide, those will stop working, with an error "Invalid Work Execute Mode".

Mitigation

The very simple mitigation for the issue is to simply go and enable the check-box to use process guide on the mobile device menu item. 

Mobile device menu items - Use process guide radio button


Note There are also a number of SYS flows that only have the Process Guide version where we did not want people to be able to use the legacy one - those are not editable on the form and will always use Process Guide

Impact

We expect that the impact of this should be very minimal, as there aren't that many Process Guide based flows live on 8.1.2 or below.
But if you do encounter this, please use the mitigation above.

Thanks

Monday, December 03, 2018

[Tutorial] Automating Release to Warehouse process through Wave Template configuration

Introduction

Currently many companies resort to manually choosing which shipments need to be added to a wave we are about to process, which is slow and error prone, as the user needs to check up on each of the shipments, understand where it is headed, what types of items are on it, which mode of delivery is used, etc.

Today I would like to show a demo example of how to configure wave templates, as to allow a more optimal distribution of orders, based on, say, delivery mode, or some other cut-off criteria. Automatically, without any user interaction.

Disclaimer

I will not be talking about Automatic release of sales/transfer orders to warehouse in this post. We'll discuss that at a later point in time. Today it is only about making sure certain orders are grouped on a single wave automatically, based on defined criteria, so that they can be processed in the warehouse at the same time. This of course comes hand in hand with automatic release, which would enable a pretty much automatic operation for certain orders

Wave template configuration

Imagine that I would like to accumulate and then process all 2-day shipping orders at once, every second day, so I have the picking done before 4 pm that day when the UPS (or other 3PL) truck arrives to pick them up.
For these orders I want to pick as fast as possible, and the inventory is typically already at the locations and does not need special packaging, so I will not include replenishment and containerization steps into it, and will configure my work template to take the goods straight to the bay doors. 
I would also like to limit the number of shipments on each of the waves to 10, so as to not overwhelm my workers. Instead, I'll release another wave later in the day if necessary.
Generally, I'd like the wave to be processed automatically as soon as 10 shipments have accumulated, but throughout the day my Wave planner might process another wave, depending on the situation.

Here's how my wave template would look for this case:

2 day shipping wave template configuration for warehouse 24

Here's how I have configured the corresponding query:
Note. No need to add any filters on Site or Warehouse, as these will be automatically added.

I want to only look at shipments for Sales orders, and only those that have a 2 DAY shipping mode of delivery. Pretty simple. If I wanted, I could make this much more granular, by adding additional table joins and adding filters on them.

2 day shipping wave template query

Sales orders

Now let's create a couple sales orders, set the Mode of delivery correctly, reserve the inventory and release the orders to the warehouse.

Note. The field that is transferred to the shipment is taken from one of two:

  • Mode field on the sales order, if the Mode of delivery is not filled in.
  • Mode set on the Carrier associated with the specified Mode of delivery.
Mode of delivery specified on Sales order 000910
I only added one simple line for the example, on both sales orders.

Sales line for Sales order 000910

Release to warehouse

When we release the first order to the warehouse, we will create a new shipment, which will have the correct Mode of delivery. We will then proceed to search for a wave template that matches it, based on Site, Warehouse, as well as the defined query. Our newly created wave template should succeed here.
We will not however be able to find an existing Wave in status Created, that would match the Site and Warehouse for the shipment that is also using the same wave template. Which means we will go and create a new Wave
Note. A new wave is created because of the Wave template setting Automate wave creation

When we release the second order to the warehouse though, we will find the wave created above, and because the conditions for adding to the same wave are met, and the wave template has Assign to open waves marked as Yes, we will add the shipment to the found wave instead of creating a new one.

Here's how the resulting wave looks:

Wave created based on the two sales orders


FAQ

Q: What if there is more than one wave in status Created, that was created based on the "right" wave template, and is for the same Site and Warehouse as the newly created shipment? Which wave will the shipment get assigned to?
A: The first of the waves that matches all of the criteria which also does not exceed the thresholds.

Q: Can I use fields directly from the sales order in the wave template query?
A: Yes, but you will need to join to get them. The right way would be to first join the load lines for the shipment, then the order lines related to the load lines, and then the sales order headers related to the sales lines. (Not directly from the shipment, even thought that will work just fine for certain businesses that always have only 1 shipment per 1 order)

Conclusion

As you can see, setting up a wave template that will group multiple shipments and create a new wave if necessary is relatively simple - you just need to get a few of the specifics correct in the setup, and then everything will work like a charm.

In one of the next posts we will combine this configuration with automatic release of sales orders, to achieve full automation of sales order release process, where no user interaction is necessary at all past the sales order creation and confirmation step.

Friday, November 16, 2018

[Development Tutorial] Tired of WHSWorkExecuteDisplay*? ProcessGuide to the rescue!

If you have ever tried extending the warehouse mobile flows in Dynamics 365 Finance and Operations, you know the code is very complex and extremely difficult to make sense of and modify without introducing regressions in unpredictable scenarios.

This was of course on our mind as well for some time, which is now I am pleased to announce the release of a new framework for implementing such flows, called ProcessGuide framework.

You can read the documentation we've published so far here:
https://docs.microsoft.com/en-us/dynamics365/unified-operations/supply-chain/warehousing/process-guide-framework

We will incrementally start converting the existing flows, and you should do the same with yours.
Give it a try and let us know if you have any feedback by leaving a comment here or reaching out directly!

Thanks