Code highlighting

Showing posts with label AX7. Show all posts
Showing posts with label AX7. Show all posts

Sunday, December 02, 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.

Wednesday, October 17, 2018

[Tutorial] Reading On-Hand information for Warehouse enabled items

Having difficulty understanding your own business data is probably not something you would expect to happen to you, but I know many people, including many 10+ AX experts who have a difficult time following the information about item availability when it comes to warehouse related items.

In this blog post, I will try to walk you through a specific example, line by line, column by column.

Note I am assuming that everyone is familiar with the general way we show on-hand information, specifically that you can summarize it by a different set of dimensions, viewing it aggregated by warehouse, location, or any other combination of dimensions.

What's the challenge?

The problems start when you need to account for the reservation hierarchy of the warehouse enabled item, and displaying how much is available at the different reservation hierarchy levels.

On-hand view, summarized by Site, Warehouse, Inventory status, Location and License plate

On-hand for item A0001, summarized by all storage dimensions


On-hand view, summarized by Site, Warehouse

On-hand for item A0001, summarized by Site and Warehouse


Step-by-step analysis of the data

  1. If we look at the Physical Inventory column, the on-hand there is always at the lowest dimension level – meaning we only see values for rows where you have all of the dimensions specified, down to the License plate level.
    1. That’s why in the first row, where there’s no location/license plate, the Physical inventory is shown as blank (0). Same for the 5th row, where for location FL-001 with blank LP there’s nothing in physical inventory. (RED)
    2. Note that for BULK-001 there is physical inventory, even though there is no LP – that’s because this location is not tracked at License Plate level, so the Location IS the lowest level (ORANGE)
    3. For some of the lines shown, there simply isn’t on-hand, but we used to have on-hand on them, which is why they show up. (YELLOW)
    4. The rest (GREEN) are quantities that are on license plates on corresponding locations. You can sum up and see the total quantity of this item we have in the warehouse is 205 (but, again, that’s not displayed in the first row, unless you change which dimensions are displayed – if you only display Site and Warehouse, for example, that’s what you would see – 205. See screenshot #2 above)
  2. Reservations are done at different dimension levels. So, for example, Sales order reservations are at “Above-location” level, while Work reservations are at “Location” level. Reservations at other levels are also possible
    1. That’s why in the first row, we have 30 in the Physical Reserved column – that is 3 different sales orders having a reservation against the quantity 10 each. They don’t care which location/license plate we take the items from, as long as we have enough on the warehouse/status level. (BLUE)
    2. Work reservation are at the location level. That’s what we have in row 5, where 2 work orders each reserve 10 units, and those units are to be picked exactly from location FL-001, but we do not care which license plate they will be taken from. (DARK BLUE).
    3. All the other rows are empty, as we have no reservation at the location+LP level here (Even though it is possible, generally speaking)
  3. Available physical column is where it gets a bit more tricky to understand. Let’s take it bottom up, so it’s easier to understand
    1. On third line, we have 100 physically there, and there are no reservations for any of it, so all of it is available. Thus, it’s 100
    2. Same for the 4th line, 10 there and all of it is available.
    3. Now, important part here. On FL-001, LP 24, I have 90 physically, but 20 is reserved. So why the heck is 75 available, instead of 70? (PURPLE)
                                                    i.     It is due to the reservation being on a higher level. The reservation is done at the location level, so when calculating available, it accounts for other license plates in this location as well, LP 000USMF-000..55 has 5 on it.
                                                   ii.     Another way to think of it – how many items can I take away from this License plate, so my existing reservations are still respected. The answer is – 75, because you’d have 15 left on this LP and 5 more on the other LP within the same location. The warehouse-level reservation is also still respected, because we have enough in other locations.
    1. On FL-001, 000USMF-00..55, we have 5 available. That’s what is physically there, and because we have enough in the other LP, all of this is available for taking for another purpose.
    2. The total quantity available at the location level matches what I described above, and equals 75, as it has 95 physically on it on multiple LPs, and 20 is reserved for work.
    3. On the warehouse level, we have 205 physically, and we need 30 for sales orders, so 175 is available to be reserved for other purposes. As you can see, that’s not a direct sum-up of the other rows. Same as the available quantity on FL-001 was not a sum of what is available at each LP. The calculation logic is the same here.

Additional information


If you are still curious for more after reading the below, check out this great 1 hour presentation my colleague Lennart did at a conference a while back, explaining the reservation engine and on-hand impact: https://www.youtube.com/watch?v=--_didmZKHo&t=4s

Saturday, October 07, 2017

Development tutorial: Extensibility: Replaceable in Chain of Command methods

Recently we announced a new and pretty powerful Extensibility feature, wrapping methods with Chain of Command in augmentation classes. This allows to write much cleaner extensions with fewer lines of code, as well as provides some extra capabilities like access to protected fields and methods of augmented object, easier way of ensuring a single transaction scope for standard and extension code, etc.

If you are not yet familiar with this feature, you are missing out. Go read about it:
https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/extensibility/method-wrapping-coc

There was one significant restriction applied (by design) to these wrapper methods:

Wrapper methods must always call next

Wrapper methods in an extension class must always call next, so that the next method in the chain and, finally, the original implementation are always called. This restriction helps guarantee that every method in the chain contributes to the result.

However, what this resulted in is a more complex implementation and "workaround-like" solutions in standard code to enable some of the commonly requested extension points, where the ISV/VAR would like to completely replace the standard logic with an alternative implementation that does the same or a very similar operation.


With Platform update 11 we have added a new attribute, which allows Microsoft (on request from multiple partnres), where it is justified, to decorate a particular protected or public method, allowing wrapper methods to not call next on it, replacing the logic of that method.

Here's how it looks:

/// 
/// Attribute used to enable or disable replacing a method in an extension class. 
/// 
/// 
/// Private methods can not be set to be replaceable even with the usage of this attribute.
/// 
public class ReplaceableAttribute extends SysAttribute
{
    boolean isReplaceable;

    public void new(boolean _isReplaceable = true)
    {
        super();
        this.isReplaceable = _isReplaceable;
    }
}

Example

OK, let's now look at an example of how this will be used.

Note. Since the attribute only appeared in PU11, that means that all application released up to and including Spring release 2017 do not have any methods marked with this attribute. It is only now with the Fall release of 2017 that you might see some methods being tagged this way.


Say, an ISV wanted to provide an alternative implementation for looking up Warehouses on a specified Site, more specifically, for the method InventLocation.lookupBySiteIdAllTypes().
One way to solve this could be to add a delegate, invoke it at the beginning of the method, and then check the EventHandlerAcceptResult to see if someone has replaced the implementation, in which case, short-circuit the method execution, so standard logic is not executed.

A potential implementation shown below:

public class InventLocation extends common
{
    public static void lookupBySiteIdAllTypes(FormStringControl _ctrl, InventSiteId _inventSiteId)
    {
        EventHandlerAcceptResult lookupBySiteIdResult = EventHandlerAcceptResult::newSingleResponse();
        InventLocation::lookupBySiteIdAllTypesDelegate(_ctrl, _inventSiteId, lookupBySiteIdResult);

        if (lookupBySiteIdResult.isAccepted())
        {
            return;
        }

        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(InventLocation), _ctrl);
        ListEnumerator listEnumerator = List::create(InventLocation::standardLookupFields()).getEnumerator();

        while (listEnumerator.moveNext())
        {
            sysTableLookup.addLookupfield(fieldName2id(tableNum(InventLocation), listEnumerator.current()));
        }

        sysTableLookup.parmQuery(InventLocation::standardLookupBySiteIdQuery(_inventSiteId));
        sysTableLookup.performFormLookup();
    }
}

Lookups is one of the common examples, where people might was a complete replacement of the standard logic. Note that by definition that means only one of the ISV solutions can replace it. If two attempt to accept() the result, an error will be shown.
That would typically mean that a logical conflict exists between the two ISV solutions, and the VAR would need to decide which ones to use, or make it configurable somehow.

Now, let's try to see what could be done with the new attribute, if Microsoft were to apply it on this method.

public class InventLocation extends common
{
    [Replaceable]
    public static void lookupBySiteIdAllTypes(FormStringControl _ctrl, InventSiteId _inventSiteId)
    {
        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(InventLocation), _ctrl);
        ListEnumerator listEnumerator = List::create(InventLocation::standardLookupFields()).getEnumerator();

        while (listEnumerator.moveNext())
        {
            sysTableLookup.addLookupfield(fieldName2id(tableNum(InventLocation), listEnumerator.current()));
        }

        sysTableLookup.parmQuery(InventLocation::standardLookupBySiteIdQuery(_inventSiteId));
        sysTableLookup.performFormLookup();
    }
}

The ISV can now in his model wrap this method in an augmentation class, provide his own implementation, and avoid calling next():

Important. 
We recommend to always make the call conditional, so that your own logic that is not calling next is only invoked for your specific case. This will make you a good citizen, that can co-exist with other ISV solutions also wrapping the same method.


[ExtensionOf(tableStr(InventLocation))]
public final class MyPU11InventLocationTable_Extension
{
    public static void lookupBySiteIdAllTypes(FormStringControl _ctrl, InventSiteId _inventSiteId)
    {
        const str MySpecialWarehouseCtrlName = 'MySpecialWarehouseCtrl';
        if (!_inventSiteId || _ctrl.name() == MySpecialWarehouseCtrlName)
        {
            // Your own logic
            _ctrl.performTypeLookup(extendedTypeNum(InventLocationId));
        }
        else
        {
            next lookupBySiteIdAllTypes(_ctrl, _inventSiteId);
        }
    }
}

Pretty easy and neat, huh?

Missing an extension point? Log it!

Again, remember, that in order to skip calling next, the method needs to be marked by Microsoft as Replaceable.
If you need a particular method to be Replaceable, or if you in general need an extension point that is not available in the latest available release, please follow the instructions outlined here to create an extensibility request for us.


Links


To review the list of features included in Platform update 11, see the What's new or changed topic and refer to the KB article for details about the customer found bug fixes included in this update.

Development Tutorial: Extensibility: Adding a table display/edit method and showing it on a form in PU11

In my previous post I described the capabilities of the Dynamics 365 FOE platform update 10, when it comes to working with display/edit methods.

In Platform Update 11 a few improvements came out, which I will describe below (with an example).

Let's use the same example as in my previous post, and add a new display method showing the internal product name for a selected product - we'll compose it by appending some text to the product search name.


Step 1 - Create a table extension and add a new display method to it - New recommended approach


All the approaches described in the previous post are still applicable, but are, in my opinion, not as intuitive as the below, so I'd recommend to always use the below approach.

[ExtensionOf(tableStr(EcoResProduct))]
public final class MyPU11_EcoResProductExtensionOfTable_Extension
{
    [SysClientCacheDataMethod]
    public display Name myInternalProductName()
    {
        return "PU11: " + this.SearchName;
    }

    // This is same as in PU10
    [SysClientCacheDataMethod]
    public static display Name myInternalProductNameStatic(EcoResProduct _ecoResProduct)
    {
        return "PU11 static: " + _ecoResProduct.SearchName;
    }
}
K/code>

OK, so what do we have here?
A simple instance method, no redundant arguments, access to table fields and methods through this.
Just as with overlayering, there's really no difference.

NoteThe logic of the method is not really important - you'd have your fields used here, most probably, but for the sake of the example I just use SearchName field.

Step 2 - Add the method to a form through a table field group


So now let's see another thing, which was not possible before PU11.

Let's create an extension for the metadata changes of the EcoResProduct table, and add a new Field Group. After that we'll put our new display method in it, as shown below.

Select the new display method as the source for the new field group field.
As you can see from the image above, you can now use the drop-down, and it will show you all the display/edit methods created in Extension/Augmentation classes as well as the standard ones.
The syntax is the same as described last time:
  • :: for static methods
  • . for instance methods (new)
All that's left is to add the new field group onto a form.

Step 3 - Add the methods to a form through extension


Same as before, we create a form extension, and add the new fields to it.
With field groups it is as easy as with overlayering - you can just drag and drop the new field group from the DataSources\EcoResProduct node onto the Design of the form, and it will create the new group and sub-controls, and set the appropriate properties on them, as shown below:

Add new field group onto the form extension
As easy as that.

You can also add the fields bound to the new display methods directly, as shown before. Except now you can also use the instance display/edit methods, which wasn't available earlier.

Add an instance extension display method to a form


Limitations

  • The drop-down for display methods does not show the extension methods, as with field groups. This should be addressed in one of the upcoming updates.
  • You are still not able to declare the display method on the form itself, or on the form data source.

Conclusion

Some last words: The future is bright! :) 
The tooling is getting better with every release, and thanks to the monthly updates and binary compatibility of the releases, new innovation from Microsoft is just a month away!

Links

You can download the project from the example from my OneDrive.

To review the list of features included in Platform update 11, see the What's new or changed topic and refer to the KB article for details about the customer found bug fixes included in this update.

Thursday, September 28, 2017

Development Tutorial: Extensibility: Adding a table display/edit method and showing it on a form in PU10

One of the super common tasks for an application developer working to address customer requirements is adding display methods showing some additional customer-specific information on existing forms.

Usually you would overlayer the corresponding table and form and insert the missing method. Overlayering is not an option soon, however it is possible to do the same using only Extensions.

As an example, let us add a new display method showing the internal product name for a selected product - we'll compose it by appending some text to the product search name.

Step 1 - Create a table extension and add a new display method to it - Option 1


As you know, there are two ways to create extension classes now, so let's see both ways in action.
Here we'll look at the "old" way, where we create an actual extension class (as in .NET), so it must be static, and the display method must be static as well, and take the record as the first argument.

Here's how it looks for my example:

/// 
/// Extension class for EcoResProduct table.
/// 
public static class MyPU10_EcoResProductTable_Extension
{
    [SysClientCacheDataMethod]
    public static display Name myInternalProductName(EcoResProduct _ecoResProduct)
    {
        return 'IntName: ' + strReplace(_ecoResProduct.SearchName, ' ', '');
    }
}

As you can see, we can define the above method and it will compile even though normally declaring a static display method is not allowed by the compiler.
We can also decorate the method with attribute, like I have done here by applying the display method caching attribute.
The logic of the method is not really important - you'd have your fields used here, most probably, but for the sake of the example I just use SearchName field.

Step 2 - Create a table extension and add a new display method to it - Option 2


So, another "new" way to extend a table is through an augmentation class, using the ExtensionOf attribute. This is shown below for my example:


/// 
/// Extension class for EcoResProduct table.
/// 
[ExtensionOf(tableStr(EcoResProduct))]
final class MyPU10_EcoResProductExtensionOfTable_Extension
{
    public static display Name myInternalProductName(EcoResProduct _ecoResProduct)
    {
        return "Alt: " + _ecoResProduct.SearchName;
    }
}

As you can see, this again is a static method - declaring it as an instance method will compile and would allow you to reference the record through this, but you will not be able to use it as a display method on the form as of today.
The method also needs to take the record as the argument.
It, of course, can also be decorated with the SysClientCacheDataMethod attribute, as in the first example.

Step 3 - Add the methods to a form through extension

Note - Limitation

You cannot as of today add the newly created display methods to a field group on the table. It will compile, but the control will not show up on the form if you add the field group to it.


First off, we'll need to create an extension of the EcoResProductDetails form in the desired model.
Then we'll add a new tab page to it and place two new String controls into it.

Now, the trick is with how to specify the display method name in Properties.

See the example below:

Specify properties for form string control to bind it to a table data method
As you can see, the trick is to specify the full name, including the class name and the method name with the static method delimiter in the format:

<class name>::<static method name>

This is only supported for table methods, so you won't be able to do the same for a Form Data Source, for example.

Result

Here's how our new awesome display methods look at run-time:

Additional information shown through display methods on Product details form

Download the project

You can download it from my OneDrive here.

What's next

In an upcoming platform update we hope to provide a much more intuitive way of adding display methods, however the above approach will keep being supported.
Stay tuned for an update!

Monday, September 04, 2017

Development tutorial: insert_recordset using the Query class

Introduction

I am sure most of you are familiar with set-based X++ CUD operators: insert_recordset, update_recordset and delete_from. They allow performing database operations with a large number of records in a single roundtrip to the server, instead of a row-by-row type of operation, which depends on the number of rows being processed. As a result, they can provide a very significant boost in overall performance of a selected flow.

If not familiar or just need a refresher, you can read more about it by following the link to MSDN.

Problem statement

There's however one significant drawback with these operators - they are all compile-time constructs, so they lack the flexibility of the flow modifying the database request before it is set based on runtime state of the flow.

And, once the application is sealed, that will mean there is no way to modify the request even at compile-time, as X++ statements are not extensible.

Solution

In Microsoft Dynamics AX 2012 R3 a static method was added on the Query class, that allows to solve the two problems above for insert_recordset. This is of course now also available in Microsoft Dynamics 365 for Finance and Operations: Enterprise edition.

Note: update_recordset and delete_from are still not supported through the Query class.

Example

Imagine that we need to write a function that would copy selected sales order line information into a sales order line history table for one or more orders.

1. Data model

Here's how the data model for this table is defined in this example:

DEV_SalesLineHistory data model diagram
DEV_SalesLineHistory data model diagram

2. New DEV_SalesLineHistory table in Visual Studio

And here's how the new table looks in Visual Studio (I created a new model for it in a separate package dependent on Application Suite):

DEV_SalesLineHistory table in Visual Studio
DEV_SalesLineHistory table
Note I skipped all stuff non-essential to this example

3. Copy function through regular X++ insert_recordset statement

Let's first write the statement for inserting the history records using the regular insert_recordset operator:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertXppInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        SalesLine               salesLine;
        InventDim               inventDim;

        JournalPostedDateTime   postedDateTime = DateTimeUtil::utcNow();
        JournalPostedUserId     postedBy = curUserId();
        SalesDocumentStatus     postingType = DocumentStatus::PackingSlip;

        insert_recordset salesLineHistory
        (
            SalesId,
            LineNum,
            InventTransId,
            SalesQty,
            SalesUnit,
            InventSiteId,
            InventLocationId,
            PostedDateTime,
            PostedBy,
            PostingType
        )
        select SalesId, LineNum, InventTransId, SalesQty, SalesUnit from salesLine
            where salesLine.SalesId == _salesId
            join InventSiteId, InventLocationId, postedDateTime, postedBy, postingType from inventDim
                where inventDim.InventDimId == salesLine.InventDimId;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, we do a simple select from SalesLine, specifying the exact fields, joined to selected fields from InventDim, where the field list also contains a few local variables to populate into the rows being inserted.
This is the standard syntax used with X++ insert_recordset statement, which all of you are familiar with.

4. Method signature for Query::insert_recordset()

Now let's convert the above to a Query, and call Query::insert_recordset() instead.
This method accepts three arguments:
  • An instance of a table record. This is where data will be inserted into. We can then use this variable to ask how many rows were inserted, for example.
  • An instance of a Map(Types::String, Types::Container), which defines the mapping of the fields to copy. In X++ operator, this had to be based on the specific order in the field selection lists in the select statement. 
    • The map key is the target field name.
    • The value is a container, which defines a pair of values:
      • the unique identifier of the QueryBuildDataSource object points to the table to copy the value from
      • the field name on the above data source to copy the value from
  • An instance of a Query class, which defines the select statement for the data, similar to what you see in the X++ version above.

As you can see from the above, it does not account for literals, as we did with variables in the X++ operator example.
That is currently not supported with this API.
We can however solve this through a use of a "temporary" table, as suggested below.

5. Define a new table to store calculable literals

Let us define a new table that will store the data required by our insert statement. That means it needs to contain four fields:
  • PostedDateTime
  • PostedBy
  • PostingType
  • SalesId - we'll use this to join to SalesLine. This could be sessionId or whatever is required to ensure concurrency and uniqueness
Here's how the table would look in Visual Studio designer:

DEV_SalesLineHistoryPostingDataTmp table definition

We can now populate this table with the required values and join it to our query.
After executing the bulk insert we can then delete the inserted row (if necessary).


Another possible implementation here could be to use a View, with computed  columns for the different literal values needed. You could select from a table that has only 1 row, like InventParameters or the like. This is however less flexible, as it'll be compiled in, while with a "temporary" table you could determine the values at runtime.

6. Write up the code using the Query::insert_recordset() method

Now we are all set to write the necessary code. It would look like below:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertQueryInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        
        Query query = new Query();
        QueryBuildDataSource qbdsSalesLine = query.addDataSource(tableNum(SalesLine));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, LineNum));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, InventTransId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesQty));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesUnit));
        qbdsSalesLine.addRange(fieldNum(SalesLine, SalesId)).value(queryValue(_salesId));
        QueryBuildDataSource qbdsInventDim = qbdsSalesLine.addDataSource(tableNum(InventDim));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventLocationId));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventSiteId));
        qbdsInventDim.relations(true);
        QueryBuildDataSource qbdsPostingData = qbdsInventDim.addDataSource(tableNum(DEV_SalesLineHistoryPostingDataTmp));
        qbdsPostingData.addLink(fieldNum(SalesLine, SalesId), fieldNum(DEV_SalesLineHistoryPostingDataTmp, SalesId), qbdsSalesLine.name());
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedBy));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostingType));

        Map targetToSourceMap = new Map(Types::String, Types::Container);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesId),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, LineNum),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, LineNum)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventTransId),     [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, InventTransId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesQty),          [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesQty)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesUnit),         [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesUnit)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventLocationId),  [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventLocationId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventSiteId),      [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventSiteId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedDateTime),    [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedBy),          [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedBy)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostingType),       [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostingType)]);

        ttsbegin;
        
        DEV_SalesLineHistoryPostingDataTmp postingData;
        postingData.PostedDateTime = DateTimeUtil::utcNow();
        postingData.PostedBy = curUserId();
        postingData.PostingType = DocumentStatus::Invoice;
        postingData.SalesId = _salesId;
        postingData.insert();
        
        Query::insert_recordset(salesLineHistory, targetToSourceMap, query);

        delete_from postingData 
            where postingData.SalesId == _salesId;

        ttscommit;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, the first part of the code builds a query using the QueryBuild* class hierarchy. The query is identical to the above select statement, with the addition of another join to our "tmp" table to retrieve the literals.
The second part populates the target to source Map object, which maps the fields to insert into to their source.
The third part actually invokes the operation, making sure we have the record populated in our "tmp" table beforehand.

Note. Because the first argument is the record we insert into, we can use it to get the RowCount(), telling us how many records have been inserted.

7. Extensibility aspect

Leaving the code just like it is does not actually make it extensible, as the partners would not be able to add additional fields to copy, or additional conditions/joins to the query. To accomplish this, we'd need to break the logic out into smaller methods with specific responsibilities, or add inline delegates to do the same. Generally speaking, you should always favor breaking the code down into simpler methods over delegates.
I've not done this in the example, as that's not the purpose, but you should follow these guidelines in production code.

8. Execute and test the code

We can execute these methods now, but first we need to ensure we have a test subject, aka a Sales order to practice with. Here's the one I used in the example:

A sales order with 3 order lines
And here's the Runnable Class that invokes the two methods above:

class DEV_Tutorial_InsertRecordset_Runner
{        
    public static void main(Args _args)
    {        
        const SalesId SalesId = '000810';

        if (SalesTable::exist(SalesId))
        {
            Counter xppInsert = DEV_Tutorial_InsertRecordset::insertXppInsert_Recordset(SalesId);
            Counter queryInsert = DEV_Tutorial_InsertRecordset::insertQueryInsert_Recordset(SalesId);

            strFmt('Tutorial Insert_Recordset');
            info(strFmt('Inserted using X++ insert_recordset = %1', xppInsert));
            info(strFmt('Inserted using Query::insert_recordset() = %1', queryInsert));
        }
    }

}

You can download the full project here.


Hope this helps! Let me know if you have any comments.

Friday, March 31, 2017

Development tutorial link: Extensibility challenges: Pack/Unpack in RunBase classes

Introduction + example

As you know, we have been focusing on extending our Extensibility story in the application, as well as trying to document the various patterns common to the application and how to address them if you are an ISV and need to extend some existing functionality.

mfp has recently written a blog post describing how you can extend the information shown on a RunBase-based dialog, and how to handle that information once the user enters the necessary data.

You can read through that particular example here: 
What that example did not describe is how to preserve the user entered data, so that next time the dialog is opened, it contains the last entries already populated. This is the typical pattern used across all AX forms and is internally based on the SysLastValue table.

In RunBase classes it is done through the pack and unpack methods (as well as initParmDefault).
For ensuring seemless code upgrade of these classes they also rely on a "version" of the stroed SysLastValue data, which is typically stored in a macro definition. The RunBase internal class state that needs to be preserved between runs is typically done through a local macro.
A typical example is shown below (taken from the Tutorial_RunBaseBatch class):

    #define.CurrentVersion(1)
    #localmacro.CurrentList
        transDate,
        custAccount
    #endmacro

    public container pack()
    {
        return [#CurrentVersion, #CurrentList];
    }

    public boolean unpack(container packedClass)
    {
        Version version = RunBase::getVersion(packedClass);
    
        switch (version)
        {
            case #CurrentVersion:
                [version,#CurrentList] = packedClass;
                break;
            default:
                return false;
        }

        return true;
    }

Just in short, what happens is that:

  • We save the packed state of the class with the corresponding version into the SysLastValue table record for this class, which means that all variables in the CurrentList macro need to be "serializable". 
    • The container will look something like this: [1, 31/3/2017, "US-0001"]
  • When we need to retrieve/unpack these values, we retrieve the version as we know it's the first position in the container.
    • If the version is still the same as the current version, read the packed container into the variables specified in the local macro
    • If the version is different from the current version, return false, which will subsequently run initParmDefault() method to load the default values for the class state variables 

Problem statement

This works fine in overlayering scenarios, because you just add any additional state to the CurrentList macro and they will be packed/unpacked when necessary automatically.

But what do you do when overlayering is not an option? You use augmentation / extensions.

However, it is not possible to extend macros, either global or locally defined. Macros are replaced with the corresponding text at compile time which would mean that all the existing code using the macros would need to be recompiled if you extended it, which is not an option.

OK, you might say, I can just add a post-method handler for the pack/unpack methods, and add my additional state there to the end of the container.

Well, that might work if your solution is the only one, but let's look at what could happen where there are 2 solutions side by side deployed:
  1. Pack is run and returns a container looking like this (Using the example from above): [1, 31/3/2017, "US-0001"]
  2. Post-method handler is called on ISV extension 1, and returns the above container + the specific state for ISV 1 solution (let's assume it's just an extra string variable): [1, 31/3/2017, "US-0001", "ISV1"]
  3. Post-method handler is called on ISV extension 2, and returns the above container + the specific state for ISV 2 solution: [1, 31/3/2017, "US-0001", "ISV1", "ISV2"]
Now, when the class is run the next time around, unpack needs to be called, together with the unpack method extensions from ISV1 and ISV2 solutions.

  1. Unpack is run and assigns the variables from the packed state (assuming it's the right version) to the base class variables.
  2. ISV2 unpack post-method handler is called and needs to retrieve only the part of the container which is relevant to ISV2 solution
  3. ISV1 unpack post-method handler is called and needs to do the same 

Steps 2 and 3 cannot be done in a reliable way. OK, say we copy over the macro definitions from the base class, assuming also the members are public and can be accessed from our augmentation class or we duplicate all those variables in unpack and hope nothing changes in the future :) - and in unpack we read the sub-part of the container from the base class into that, but how can we ensure the next part is for our extension? ISV1 and ISV2 post-method handlers are not necessarily going to be called in the same order for unpack as they were for pack.

All in all, this just does not work.

Note

The below line is perfectly fine in X++ and will not cause issues, which is why the base unpack() would not fail even if the packed container had state for some of the extensions as well.

[cn, value, value2] = ["SomeClass", 4, "SomeValue", "AnotherClass", true, "more values"];

The container being assigned has more values than the left side.

Solution

In order to solve this problem and make the behavior deterministic, we came up with a way to uniquely identify each specific extension packed state by name and allow ISVs to call set/get this state by name.

With Platform Update 5 we have now released this logic at the RunBase level. If you take a look at that class, you will notice a few new methods:
  • packExtension - adds to the end of the packed state (from base or with other ISV extensions) container the part for this extension, prefixing it with the name of the extension
  • unpackExtension - look through the packed state container and find the sub-part for this particular extension based on extension name.
  • isCandidateExtension - evaluates if the passed in container is possible an extension packed state. For that it needs to consist of the name of the extension + packed state in a container.
You can read more about it and look at an example follow-up from mfp's post below:

Hope this helps!

Thursday, March 02, 2017

Extensible enums: Breaking change for .NET libraries that you need to be aware of

A while back I wrote a blog post describing the Extensible Enums - a new feature that is part of Dynamics 365 for Operations:
http://kashperuk.blogspot.com/2016/09/development-tutorial-extensible-base.html

I explained that when marking an enumeration as extensible, the representation of this enum under the hood (in CLR) changes. Here's the specific quote:
The extensible enums are represented in CLR as both an Enum and a Class, where each enum value is represented as a static readonlyfield. So accessing a specific value from the above enum, say, NumberSeqModule::Invent would under the hood look something like NumberSeqModule_Values.Invent, where Invent is of type NumberSeqModule which is an Enum. It would in turn call into AX to convert the specific named constant "Invent" to its integer enumeration value through a built-in function like the symbol2Value on DictEnum.
Something that was not super clear in the post is that this was actually a breaking change that might impact your .NET solutions relying on one of these base enumerations.

Problem statement

As part of enabling further extensibility in the application for the next release, we have made a number of additional base enums extensible.

Let's take enum TMSFeeType as an example. Assume we have made it extensible in X++. That means that in our C# project, where we use this enum, we will no longer be able to access it from Dynamics.AX.Application namespace by name, like so:

switch (accessorialFeeType)
{
    case TMSFeeType.Flat:
        // Do something
        break;

    case TMSFeeType.PerUOM:
        // Do something else
        break;
    // etc.
}

If you navigate to its definition, you will notice that the enum declaration is empty:

namespace Dynamics.AX.Application
{
    public enum TMSFeeType
    {
    }
}

The proper way to use the enum that is extensible is to reference the above mentioned class suffixed with _Values, which lives in the Dynamics.AX.Application.ExtensibleEnumValues namespace, like so:

if (accessorialFeeType == TMSFeeType_Values.Flat)
{
    // Do something
}
else if (accessorialFeeType == TMSFeeType_Values.PerUOM)
{
    // Do something else
}

Note: Because the values are now determined at runtime by going to the AOS and asking for the correct integer value of this enum, they cannot be used in a switch/case block, which expects constant expressions known at compile-time.

What's next

Obviously, this situation is not great. 
Let's hope that Microsoft will think of a good way to address this going forward.

Question to you

That leads to a question - how many of you actually have .NET libraries relying on application code in Dynamics 365 for Operations and might be impacted by us making some of the enums extensible in the next major release?

Wednesday, December 28, 2016

Configuration tip: Warehouse mobile devices portal Native application in Azure Active Directory

Setting up the Warehouse mobile devices portal is a relatively simple process, and is described in detail on the corresponding wiki:
https://ax.help.dynamics.com/en/wiki/warehouse-mobile-devices-portal-for-microsoft-dynamics-ax/

However, now and again I would get complaints around the authorization process not working properly, or asking for interactive login, both of which shouldn't be necessary for the authorization method used by WMDP.

So in this post I would like to give a small configuration tip that prevents the below error message:

Error: "The user or administrator has not consented to use the application with ID ''. Send an interactive authorization request for this user and resource.

The reason this error is shown is because the Native application setup in Azure for WMDP has access to resources that require the Azure admin to consent to that first.

WMDP app is configured to allow access to AAD resources
As you can see, some of the resources WMDP app has access to require prior Admin consent.

Recommendation: 

When configuring the Native application for WMDP in Azure, only give it permissions for the Microsoft Dynamics ERP resources.

This will ensure you don't get unexpected errors when trying to connect to WMDP.



Happy Holidays to all of you!

Stay tuned for more posts next year and subscribe if you haven't already to get updates on the latest tips and tutorials for Dynamics 365 for Operations aka Dynamics AX aka Axapta :)

Friday, November 04, 2016

Tutorial Link: Executing outbound work with pending demand replenishment work

Introduction

In Dynamics 365 for Operations we solved one of the long-standing complaints, where large work orders could not be started because of pending replenishment. A typical workaround then would be to artificially broke down the replenishment lines into a separate work order, so workers can do the picking for the majority of stuff. Then of course you'd get into problems with merging the two (or more) Target LPs onto one (which we now also support - see my previous blog post).

Read the feature description and and walk through a sample flow on our SCM blog:
https://blogs.msdn.microsoft.com/dynamicsaxscm/2016/11/04/processing-work-that-is-awaiting-demand-replenishment/

For those on AX 2012 R3

We have not back-ported this feature to AX 2012 R3 yet. We have it in the backlog, but no ETA for when that will happen.

Update: This is now available on LCS under KB3205828

Feedback

We'd love to hear your feedback on this feature if you are going to use it in your production environments.



Wednesday, November 02, 2016

Tutorial: License plate consolidation in Dynamics 365 for Operations (1611)

Introduction to scenarios

The Microsoft Dynamics AX Warehouse management module supports a number of advanced scenarios for warehouses, thanks to the flexibility offered by the concept of Work and work lines, which take care of any and all operations in the warehouse. The system has a number of complex configuration possibilities that determine how work is created. At the same time, AFTER it has been created, work is fixed and any necessary changes require a lot of manual supervisor interventions, or, in many cases, are not possible at all.

Imagine the following two scenarios:

Scenario 1

A customer orders a number of items from us. Based on our work template setup, multiple work orders are created to pick these items from the warehouse, say, some are picked from a cooled area, while the rest is coming from the regular picking area. All of the items are placed into a staging area location after picking, to be loaded on a delivery truck.

Scenario 2

A customer orders a number of items from us. A sales order is created in our system, released to warehouse, so work is created. The picking commences, and the goods arrive at the staging location, but are not shipped the same day, because the truck to pick them up only arrives tomorrow. The same evening, the same customer orders more items from us. Correspondingly, a new sales order is created and released to warehouse, creating more work. The pick is completed, and the goods are placed in the staging location ready to be loaded the next day. The warehouse manager decides to ship both shipments on the same truck tomorrow, adding the second shipment to the same load that contains the first shipment.


In both scenarios, we now have two license plates on the same load, that are going to the same customer, sitting in the same location. When the truck comes, they will need to be loaded separately one by one, even though in many cases, the items would fit perfectly fine on just one license plate.
Until now, the warehouse worker had no way to merge the two license plates together to ship everything on just one license plate, at least in the system (probably does happen outside of the system already).

In the Fall release of Microsoft Dynamics 365 for Operations (1611) we have added the ability of consolidating items on a license plate with items on another license place within the same location, where there is work behind one or more of the license plates. This is supported by a new type of mobile device menu item with Indirect Activity code “Consolidate license plates”.

Scenario walkthrough

All you need to do to enable this mobile device flow is create a new mobile device menu item that looks like the screenshot below:


LP consolidation mobile device menu item
For the scenario walk-through I have created two sales orders as below:
  • Sales order 000781 for customer US-001, which contains two lines:
    • 10 ea of item M9200 from warehouse 51
    • 15 ea of item M9201 from warehouse 51
  • Sales order 000782 for customer US-001, which contains one line:
    • 7 ea of item M9201 from warehouse 51

Sales order 000781 was released to warehouse first, before 000782 existed, resulting in the creation of Shipment USMF-000008 on Load USMF-000010.
Sales order 000782 was then added to the same Load, and also released to warehouse, resulting in the creation of Shipment USMF-000009.


The following picking work was created for these 2 shipments:

Work order details
This scenario corresponds directly to Scenario 2 I described above.

Now, for both work orders the initial picks are executed (one or different workers), and for both the goods end up at the STAGE location, awaiting loading, as shown below:

Work order details after initial pick was executed
From here on, license plates TLP_001 and TLP_002 follow the same path in the warehouse, more specifically they both will be picked up from STAGE and loaded into the truck at BAYDOOR location.

If the warehouse worker at the outbound dock makes the decision to consolidate these two license plates (currently, this is only an ad-hoc decision by the worker, planned consolidations are not supported), he can do that using the above mobile device menu item. Here is how the flow would look on the mobile device:

Step 1

You are presented with a screen, where you need to enter the Target LP. This is the License plate, where the items will end up after the consolidation.
  • This can be a new License plate. There is no way to print this new LP Tag from this screen at this point, however, so keep that in mind.
  • This can be an existing empty License plate. This could, for example, be useful, if you want to merge the contents of 2 half-pallets onto 1 empty euro pallet. Another case is if the pallet, where the items currently are placed, is damaged or “unshippable”.
  • This can be an existing full License plate, which is set as a Target LP on an existing work order with Work order type “Sales order” or “Transfer order issue
  • This should not be work that has a Container Id on the header or any of the lines.

LP Consolidation, Step 1

Step 2

Target LP was accepted, and now you are asked to scan in the LP you want to merge onto the Target LP.
  • LP to merge needs to be a target license plate for an existing work order of Work Order Type  Sales Order or Transfer Order Issue.
  • The LP to merge needs to be in the same location as the Target LP, and it cannot be the final shipping location (that’s too late, since goods are already Picked at this point).
  • The LP to merge needs to relate to the same Load as the Target LP
    • It should also have the same delivery information, if there are more than one shipment involved. Namely, the Delivery name, Customer account, Delivery address and Mode of Delivery should match.
  • The remaining steps in the flow for both work orders being merged need to match. This is done so we ensure all relevant steps are executed, for example, that the labels are printed at the appropriate time in the flow for all consolidated items.
  • This should not be work that has a Container Id on the header or any of the lines.
LP consolidation, Step 2

Step 3

When LP to merge is accepted, the worker is presented with a confirmation screen, that shows a summary of all items on that license plate. This is to help ensure that he scanned in the right LP before the actual consolidation of the two work orders happens.

In the case below, there was only one item on TLP_002, with a total quantity of 7.00 ea

LP consolidation, Step 3

Step 4


Once the worker confirms by pressing OK, the consolidation is executed, merging the two work orders together and moving all items from LP to merge to the Target LP.


The worker gets a confirmation message that the license plates were merged, and is presented with a screen where he can continue scanning in any following license plates to merge onto the Target LP.

LP consolidation, Step 4

Here is how the work looks after consolidation happens:


The work related with LP to merge, in our scenario walk-through that is TLP_002, is now marked as Closed, and by reviewing the work lines you can see the final Put step was changed, so it now points to STAGE location instead of BAYDOOR. The only thing that changed is which license plate it is put onto, namely TLP_001 instead of TLP_002.

Work order details after LP consolidation - Merge From work
The work related with Target LP, in our scenario walk-through that is TLP_001, is still In progress, and you can see the quantities on the remaining Pick/Put pair were increased accordingly with the quantity from work related with TLP_002.

Work order details after LP consolidation - Consolidated work

If you review the corresponding work transactions, you will see that an extra transaction corresponding to the load line for M9201 from sales order 000782 has been added to the Pick line to represent the additional Pick quantity.

Accordingly, the inventory transactions also reflect the fact items were moved from TLP_002 to TLP_001, and the new work reservations are based on that as well.

Mobile device menu item configuration option “Cancel remaining origin work lines

We expect most companies to run with this configuration option turned on. It comes into play in the following situation: When there are more subsequent staging steps on both work orders, this configuration will enable you to forfeit any of the steps on the work being merged from, so the steps on the consolidated work will be the ones executed for all merged items. That specifically means that any “extra” steps on the work being merged will be Cancelled.

If there are some specific reasons why all work steps need to be executed for the merged work order separately, you should not enable the configuration option. As a result, however, you will not be able to consolidate this LP to another one.

See a more detailed explanation in the Help Text for this configuration on the menu item.

For those behind on updates :)

This feature has been back-ported to Microsoft Dynamics AX 2012 R3.
You can download it from KB number 3190562


Let us know what you think of this new feature!