D365 X++: I’m using InventABC as my template

Hi fellow developers.

Back in the early days I was at a good old Damgaard conference, and attended at a technical session. In this session one of the founding fathers of Axapta came up with the phrase “Copy with pride“. What he meant was to look at existing code and patterns in the X++ code, and to feel free to used and copy these patterns from the Microsoft code and use them in customer customizations and extensions. I have lived by this principle and “copied with pride” and encourage other to feel free to take my work and copy what they need.

One of the more common customization/extension requests is where we have a class that does some magic and have a dialog, query and can run in a recurring batch etc. I guess all developers have their own approach, and as the blog name suggest I’m using the InventABC* classes as a “copy with pride” for my code. I know this code is more than 20 years old, and I guess there are better and advanced patterns to use. I don’t say this pattern is the best, but for me it is a fast and easy way to create simple periodic recurring classes. Please any feedback is welcome, and feel free to share your approaches.

The InventABCUpdate do contain the code to creating a dialog, executing in batch, parameters and most of the code I need to create the code needed. After done, my final code does not resemble the original InventABCUpdate code, but for me it is a journey where I have fixed starting point and then make constant adjustments until I have reached the desired solution.


For any of you that is starting the journey of learning development in X++, and quite quickly being able to create solutions our customers it is well worth investment to study of the InventABC to classes and then speed up your coding experience.

Happy coding friends.

 

EDIT: The community responds quickly A better class to use could be Tutorial_RunBaseBatch

D365: Search for code with Agent Ransack

When supporting customer’s we often can get small fragments of information on an issue, like a form is not performing as expected, or an error message. The procedure is then often to log into LCS and find traces of the issue. Often we end up with a query that is the source of the issue. But to better understand and analyze how to fix the issue we often need to find exactly in the source code where the query is executed. By also being more exact and precise towards Microsoft support you also get quicker response.

Searching through the code in Visual Studio can be time consuming, and the built in Cross reference is not always updated, but there is an alternative I can recommend. Agent Ransack is a free file searching utility that quickly can scan most D365 source code (the *.XML files placed in K:\AosService\PackagesLocalDirectory\).

Let’s say I see in the LCS that the current query is what I need to find out from here it is executed.

From the query I can then search for the text “Join RetailEODTransactionTable”, and I get 25 results, and even where the exact table is not specified as

I can then open the file in explorer and then validate to see if I need to go into Visual Studio for further analysis.

This speed up the process of finding the source code that you are looking for. It is free and download it from https://www.mythicsoft.com/agentransack/ and install it in you development environment.

 

Take care Daxer’s.

D365F&O – Community Driven Engineering

I have previously blogged about the importance of reporting new ideas, issues and bugs to Microsoft, and also why the community will benefit from sharing. I see that experienced engineers have the solution available and are more than willing to give it for free to get the fixed-up code into the standard solution to benefit customers and future projects.

 

But the formalized support path does require time and energy and remember that not all Microsoft support consultants are engineers that you can discuss X++ topics with. But how can the process of contributing to the D365 community be easier?

But did you know that Microsoft have a program for Community Driven Engineering with Dynamics 365 F&O? This covers not only bugs, but also new features. Community driven engineering (CDE) is a Microsoft effort to make external engineers more efficient at providing recommended bug fixes as minor features to Microsoft, as well as to make Microsoft more efficient in accepting fixes from the community. If the fix is accepted, it will be merged into the main Dynamics 365 F&O branch. I have tried the program, and reported in a fix for auto-report as finished, and the fix was accepted, and hopefully in the near future the entire community can benefit from it.

How to start?

If you have the right skills and the willingness to share and give away your fixes (or features) you can sign up at https://aka.ms/Communitydrivenengineering. You need to be accepted into the program, and your user must be whitelisted before you can access. The CDE also have a private Yammer group, that you get access to when accepted. But I must warn you. This program is meant for the most experienced and technical people we have in our community, and that are deep into X++ and AzureDevOps. You must have approval from CxO-level in your organization that you can share code with Microsoft. (Lawyer stuff)

Here is the overall flow for the external engineer:

  1. You create a bug or a Feature in CDE Azure DevOps
  2. The bug or Feature is reviewed by the MS team and accepted or rejected
  3. You create a branch for this work and commit in this branch
  4. When done you create a Pull Request
  5. The Pull Request is reviewed by the MS team and feedback is provided
  6. After some iterations the Pull Request will be approved and complete
  7. The MS team will take over the code and include in a future release

Here are the more technical details of how it works.

The following text is copied from the onboarding documentation of the CDE.

It takes approximately one hour to get started with CDE, the majority of which is the initial build time.

  1. Obtain a development VM from LCS with build 8.1.195.20001 (app 8.1, platform update 22) or later. The latest branch I have access to is 10.0.80.19, that basically is 10.0.2 PU 26.
  2. Make sure you have opened Visual Studio at least once on the VM to sign in and pick default settings.
  3. Install Git on the machine from https://git-scm.com/downloads . The default installation options should work fine.
  4. From an administrator command line instance, clone this repo to a location on the machine.
    pushd k:\
    mkdir git
    cd git
    git clone https://dev.azure.com/msdyncde/_git/cde

  5. Define your user name and email in Git
    git config –global user.name “John Doe”
    git config –global user.email johndoe@example.com

  6. Mount the git repo into the F&O deployment
    pushd K:\git\cde
    powershell .\Mount.ps1
  7. Open Visual Studio as administrator and rebuild the following models
    ApplicationSuite
    ApplicationWorkspaces
    FiscalBooks
    GeneralLedger
    Project
    Retail
    Tax

At this point you can start development(in the SYS layer actually)

How to submit a change?

Changes submitted by the community are committed to the same REL branch matching the version on the dev VM. Once the pull request (PR) is completed, that signals that Microsoft has officially accepted the change and it will show up in a future official release, usually the next monthly release (depending on what day of the month the release closes). The change will only enter the master branch of msdyncde through a future official release. Syncing to the tip of a REL branch will pull in other community changes submitted from that version.

  1. Create a Bug or Feature depending on whether the change is related to incorrect behavior of existing code, or new behavior.
    https://dev.azure.com/msdyncde/cde/_workitems
    New work item > bug
    Fill in the title, assign it to yourself, and set the Area to your best guess as to where the behavior belongs (will help us review appropriately)
    In repro steps and system info, provide information on why this change is necessary
  2. In Git, create a topic branch to work on. Branches are usually named by username/bug number.
    git checkout -b johndoe/482
    git push –set-upstream origin johndoe/482

  3. In Visual Studio make changes to Application Suite SYS code as normal. Changes are actually being made directly in the Git folder.
  4. Push your changes to VSTS.
    git add -A
    git commit -m “Message explaining what is being changed”
    git push

  5. Send a pull request from VSTS
    https://dev.azure.com/msdyncde/_git/cde/pullrequests?_a=mine
    New pull request
    Source branch = johndoe/482, Destination branch = Rel_8.0.30.8022 (or whatever version you have)
    Fill in the title and description, link the work item > Create

Any feedback from Microsoft reviewers (or other Community reviewers) will show up in the PR. Changes can be made to the PR by editing in Visual Studio, and doing git add / commit / push again. Once Microsoft has signed off, all comments have been resolved, a work item is linked, and all other polices have been met, then you can click Complete to complete the pull request. When a PR is completed, that is official acceptance by Microsoft that the change will become part of a future official release, usually the next monthly release.

Behind the scenes

  • The powershell script starts by checking what version of source code exists on the VM by examining the K:\AosService\PackagesLocalDirectory\ApplicationSuite\Descriptor\Foundation.xml file.
  • It then checks out the REL branch associated with that version, which matches the platform and other model versions currently on the machine.
  • The development config files are updated to allow changes to SYS models, which is normally disallowed on dev VM’s.

In addition to having an accelerated approach to get fixes into main branch, participants also have some more benefits. You will have access to the latest & greatest code changes through all code branches that Microsoft makes available. You can search through the code and see if there are code changes that affects extensions or code that is local to you installations. You can also see how the Microsoft code is evolving and improvements are made available in the standard application. You will also build gradually very valuable network towards the best developers in the world, where you will discuss technical topics with the actual people creating the world’s best ERP-system.

One final joke for those considering going into this program: Git and sex are a lot alike. Both involve a lot of committing, pushing and pulling. Just don’t git push –force


Retail assortments and planned orders extensions

Microsoft have created an excellent description of this in the Assortment management doc-page. Retail assortments are essential to define what products that should be available across retail channels. Assigning a product to an assortment, will assign the product to the stores that have the assortment. This makes it possible to sell the product in the store.

But there is something missing, and that is to use assortments for replenishment and procurement. Retailers want the master planning to only suggest procurement and transfers on products that belongs to the stores assortments. You can control requirement parameters per warehouse, but there is no standard way to use the assortment to control the generation of planned orders.

This blog post is to show how to make a very small extension that allows you to make sure that only products that belongs to a stores assortment will generate planned orders. The solution I will make use of is to look into how the product lifecycle state functionality is working, and extend this with an assortment planning parameter. I have called this parameter “Is lifecycle state active for assortment procurement

What it will do, is to validate if a product is in the assortment of the store, and if it is, then the product will be requirement calculated and will generate planned orders. If the product is not in the assortment of the store, that no planned orders will be generated.

To make this happen, I needed to create 4 extensions. The three first is adding a new field on the product lifecycle form.  For an experienced developer this is easy to create, and no need to spend time on in this this blog-post.

The real fun is how to manage and control the planned order generation. This happens in in an extension to the ReqSetupDim class.  Here there is a lookup to the assortment on the product, and checks if the store have this assortment.  If not, then the masterplanning will not generate any planned orders. I therefore create an extention class and use the new method wrapping/CoC feature to add some additional code.

///
<summary>
/// Contains extension methods for the ReqSetupDim class.
/// </summary>

[ExtensionOf(classStr(ReqSetupDim))]
final class ReqSetupDim_extension
{

    ///
<summary>
    /// Validates if a product should be assortment planned
    /// </summary>

    /// The parm of the ReqSetupDim class.
    /// false if the product is not assortment planned; otherwise, return default value.
    public boolean  mustReqBeCreated(InventDim _inventDimComplete)
    {
        Boolean ret = next mustReqBeCreated(_inventDimComplete);

        if (ret)
        {
            if (inventdim.InventLocationId)
            {
                InventTable                 inventtable;
                EcoResProductLifecycleState ecoResProductLifecycleState;

                //Fetching fields from  inventtable
                select firstonly ProductLifecycleStateId, Product from  inventtable where inventtable.ItemId == this.itemId();

                //validating if the product is active for planning and that also assortment planning is enabled.
                select firstonly RecId from ecoResProductLifecycleState
                        where   ecoResProductLifecycleState.IsActiveForAssortmentPlanning == true
                            &amp;&amp;  ecoResProductLifecycleState.IsActiveForPlanning == true
                            &amp;&amp;  ecoResProductLifecycleState.StateId == inventtable.ProductLifecycleStateId;

                if(ecoResProductLifecycleState)
                {
                    RetailStoreTable                    store;
                    EcoResProduct                       product;
                    RetailAssortmentLookup              assortmentLookupInclude;
                    RetailAssortmentLookup              assortmentLookupExclude;

                    RetailAssortmentLookupChannelGroup  assortmentLookupChannelGroupInclude;
                    RetailAssortmentLookupChannelGroup  assortmentLookupChannelGroupExclude;

                    //Finding OMOperatingUnitID from the inventlocationId
                    while select firstonly OMOperatingUnitID from store
                        where store.inventlocation == inventdim.InventLocationId
                    {
                        //Check if the product is in the assortment of the store in question
                        select RecId from product
                            where product.RecId == inventtable.product
                        exists join assortmentLookupInclude
                            where   assortmentLookupInclude.ProductId == product.RecId
                                &amp;&amp;  assortmentLookupInclude.lineType == RetailAssortmentExcludeIncludeType::Include
                        exists join assortmentLookupChannelGroupInclude
                                where   assortmentLookupChannelGroupInclude.OMOperatingUnitId == store.OMOperatingUnitID
                                    &amp;&amp;  assortmentLookupChannelGroupInclude.AssortmentId == assortmentLookupInclude.AssortmentId
                        notexists join assortmentLookupExclude
                            where   assortmentLookupExclude.ProductId == product.RecId
                                &amp;&amp;  assortmentLookupExclude.lineType == RetailAssortmentExcludeIncludeType::Exclude
                        exists join assortmentLookupChannelGroupExclude
                            where   assortmentLookupChannelGroupExclude.OMOperatingUnitId == store.OMOperatingUnitID
                                &amp;&amp;  assortmentLookupChannelGroupExclude.AssortmentId == assortmentLookupExclude.AssortmentId;

                        if (!product)
                        {
                            ret = false; //The product does NOT belong to the stores assortment, and should not be planned
                        }
                    }
                }
            }
        }
        return ret;
    }
}

I also have code to restrict creation of manual purchase orders, and where simular code can be used, but let’s hope that Microsoft can further extend standard Dynamics 365 with assortment based procurement planning.

Copy with pride, and let’s hope next year will give us 365 more opertunities.

DAX2012 R3 – Playing with Retail CRT

The Microsoft Dynamics AX commerce runtime (CRT) serves as the engine for a retail channel. It connects to a CRT database that stores business data for the channel.. Dynamics AX uses the CRT Architecture in the Retail ePOS, mPOS and in the Sharepoint based eCommerce solution. Also the SiteCore eCommerce solution uses the CRT as the integration to Dynamics AX.

Let’s look at some of the interesting things with CRT.

1. It is a well-documented framework supported by Microsoft. More details on it is available here.

2. It can make it easier to integrate business logics to other eCommerce solutions. Like Magento.

3. We can create CRT web-services that handles information like very advanced retail prices, on-hand shipment, check-outs etc.

4. And MUCH more. This CRT we will have almost everything we need to interact with external system. It Rocks !

Let’s take an example. I would like to utilize the CRT to calculate a sales price with discounts. Price/discounts in AX is very flexible and almost all options you can imagine. Here is a small example of the possibilities we get in Dynamics AX 2012 R3 CU 8.

Let’s say we would like to expose this price/discount structure as a web-service to external parties. Since the CRT is relying on it’s own channel database, it means that the much of the integration is actually not creating a heavy load on the Dynamics AX database. We also have the possibility to “scale out” on the architecture.

The CRT is developed in Visual Studio, and is basically just a bunch of dll’s. I have to admit, that Visual studio, Web-services and is not my strongest side. So I decided to see it I could access and use the CRT framework directly from Dynamics AX. The new call-center module in AX 2012 R3 CU8 is using this, and I wanted to create a very simple job that exemplifies how CRT operates and can be used. Basically I’m looking for this:

I use the standard AX 2012 R3 CU8 contoso demo data, but added the following quantity discount to the Houston store;

Since I know X++, I’ll use X++ job to try this. The infolog I got was the following:

Here we see that the CRT returned the sales price, discounted price, and also information like total amount. Surely it can give everything else, but I wanted to keep the example easy to understand.

I created the following job to play with the framework, and it gives you an idea of how to interact with the CRT from AX.

static void demo_CRT_GetSalesPrice(Args _args)
{
    //To test this code, use the AX 2012 R3 CU8, and use the USRT company
    Microsoft.Dynamics.Commerce.Runtime.Data.IPricingDataManagerV2                pricingManager;
    Microsoft.Dynamics.Commerce.Runtime.Services.PricingEngine.DiscountParameters priceParameters;
    Microsoft.Dynamics.Commerce.Runtime.DataModel.SalesLine                       crtSalesLine;
    Microsoft.Dynamics.Commerce.Runtime.DataModel.SalesTransaction                crtSalesTransaction;
    System.Object                                                                 roundingRule, currencyConverter;
    CLRObject                                                                     clrSalesLines, enumeratorSalesLine;
    CLRObject                                                                     clrObjectSalesLines;
    System.DateTimeOffset                                                         activeDateTimeOffset;
    System.String                                                                 lineIdString;
    System.String                                                                 customerPriceGroup = "";
    Price                                                                         price;
    Qty                                                                           qty;
    InventDim                                                                     inventdim;

    RetailStoreTable    storeTable = RetailStoreTable::find("HOUSTON");   //Lets use houston as an example here
    RetailChannelRecId  channelId = storeTable.RecId;                     //This is the recid for a retail channel. A retail store is an extension of a retail channel.
    CurrencyCode        currencyCode = storeTable.Currency;
    appl.setDefaultCompany(storeTable.company(), false);                  //Setting default company to houstons company

    //Creating a unique line ID
    lineIdString = System.Guid::NewGuid().toString();

    InventDim.InventLocationId = storeTable.inventLocation;
    InventDim.InventSiteId     = InventLocation::find(storeTable.inventLocation).InventSiteId;
    InventDim                  = InventDim::findOrCreate(InventDim);

    //Pricing manager : Encapsulates data access to pricing information.
    pricingManager          = Microsoft.Dynamics.Commerce.Headquarters.RetailProxy.AxPricingDataManager::BuildDataManager(channelId);

    //Price parameters : types of AX discounts (aka trade agreement discounts, aka not Retail Periodic Discounts) are currently activated and should be allowed on the transaction
    priceParameters         = Microsoft.Dynamics.Commerce.Runtime.Services.PricingEngine.DiscountParameters::CreateAndInitialize(pricingManager);
    roundingRule            = Microsoft.Dynamics.Commerce.Headquarters.RetailProxy.ServiceHelpers::GetRoundingMethod(currencyCode);
    currencyConverter       = Microsoft.Dynamics.Commerce.Headquarters.RetailProxy.ServiceHelpers::GetCurrencyConverter(currencyCode);
    activeDateTimeOffset    = RetailPricingEngineHelper::getSessionDateTimeInChannelTimeZone(channelId);

    //Next we create a crtSalesLine, that represents a channel agnostic sales line.
    crtSalesLine = new Microsoft.Dynamics.Commerce.Runtime.DataModel.SalesLine();

    crtSalesLine.set_ItemId("0001");        //Lets use product 0001 as our example
    crtSalesLine.set_InventoryDimensionId(InventDim.inventDimId);
    crtSalesLine.set_Quantity(10);          //Lets use quantity 10 as our example
    crtSalesLine.set_LineId(lineIdString);  //Let give this line a uniqe ID

    crtSalesLine.set_UnitOfMeasureSymbol("ea"); //Let use 'ea' as the unit

    //We then create a sales transaction, and add the salesline to this.
    crtSalesTransaction = new Microsoft.Dynamics.Commerce.Runtime.DataModel.SalesTransaction();
    clrObjectSalesLines = crtSalesTransaction.get_SalesLines();
    clrObjectSalesLines.Add(crtSalesLine);                      //This is where the we add the salesLine to the sales transaction

    //Lets calculate the prices
    Microsoft.Dynamics.Commerce.Runtime.Services.PricingEngine.PricingEngine::CalculatePricesForTransaction(
        crtSalesTransaction,
        pricingManager,
        roundingRule,
        currencyConverter,
        customerPriceGroup,
        currencyCode,
        false,
        activeDateTimeOffset);

    //Lets calculate the discounts
    Microsoft.Dynamics.Commerce.Runtime.Services.PricingEngine.PricingEngine::CalculateDiscountsForLines(
        pricingManager,
        crtSalesTransaction,
        roundingRule,
        currencyConverter,
        currencyCode,
        '',
        '',
        '',
        true,
        Microsoft.Dynamics.Commerce.Runtime.DataModel.DiscountCalculationMode::CalculateAll,
        activeDateTimeOffset);

    //And, let's show what the CRT can give us.
    qty   = crtSalesLine.get_Quantity();
    Price = crtSalesLine.get_Price();
    info(strFmt("Normal sales price is %1 when buying %2", price, qty ));

    Price = crtSalesLine.get_DiscountAmount();
    info(strFmt("Discount amount pr unit is %1, when buying %2", price/qty, qty ));

    Price = crtSalesLine.get_NetAmountPerUnit();
    info(strFmt("Net amount pr unit is %1, when buying %2", price, qty ));

    Price = crtSalesLine.get_TotalAmount();

    info(strFmt("Total amount is %1 when buying %2", price, qty ));

Maybe some of you can teach me how to expose the CRT in a web-service, so that we can easily

 

 

 

Dynamics AX 2012 R3- To exist or not, that is the question!

The advanced filter and query in Dynamics AX is a very powerful tool. Here you can search on most fields, and also add relations to the query.

You can open this advanced query screen from almost any AX screen, and the short-cut is “CTRL-F3”.

But there is one area that the advanced query screen is not handling. That is “Not-exists-join”. Let’s say I want a list of all the purchase orders that DON’T have purchase lines. The standard AX will not help you here. Often a developer must come inn and create a smart query or script to find this. With the introduction of attributes and retail categories, I see the need for this “not-Exists-join” feature to be able to create advanced queries.

I don’t accept this, because I want to empower the end-user to perform such queries. So here is my gift to the Dynamics AX ecosystem; NotExistsJoin directly on the advanced query screen:

Then I can look at the filtered purchase list page, and I only see purchase orders that DON’T have any purchase lines J

This will work allover dynamics AX, even on cue’s and on other query based functionality.

So what’s the trick? A few changes to the following two classes:

Let’s take the changes “one-by-one”, or you can download the code here : https://dynamicsax2012r3notexists.codeplex.com/

Then here is the last change to get a icon on not-exists join:

Have fun DAX’ing 😉

Dynamics AX 2012 R3: Global recommended retail price

Maintaining retail prices in a multi company and franchise scenario can be a challenge in Dynamics AX 2012 R3. Let say we have a retailer with stores in many countries. We often also see owner structures that makes us model the retail chain with many companies.

I’m currently in an implementation to an international retailer present in 22 countries, and it keeps growing every day. To model this we need to create one Dynamics AX company per country and per partner.

The ability to maintain and control prices in such a scenario is a challenge, because prices is a property of a released item. (PriceDiscTable)

So we decided to create very small customization so that we could keep track of prices and item barcodes on the product instead of the released product level.

By doing this, the product managers have one screen where they can create and maintain product prices. The prices can be specified per currency, country and company/partner. But this is not just a simple table, but a two-way frontend for populating the standard AX price tables.

We therefore have a shared table:


The code for publishing prices into standard AX, using the price journal posting looks like this:


void publishProductRetailPrices()
{
    #Admin
    #Zebra
    DataArea                            dataArea;
    PriceDiscTable                      priceDiscTable;
    PriceDiscAdmTable                   priceDiscAdmTable;
    PriceDiscAdmTrans                   priceDiscAdmTrans;
    PriceDiscAdmCheckPost               priceDiscAdmCheckPost;
    InventDimId                         inventDimId;
    Ledger                              ledger;
    CompanyInfo                         legalEntity;
    LogisticsAddressCountryRegionId     countryRegionId;
    #define.ImportPriceDiscJournalName("Prices")
    PriceDiscAdmName                    priceDiscAdmName;

    ttsbegin;
    while select Id from  dataArea where dataArea.Id != #DATCompany
    {
        changecompany(dataArea.Id)
        {
            legalEntity             = CompanyInfo::find();
            ledger                  = Ledger::findByLegalEntity(legalEntity.RecId);
            countryRegionId         = CompanyInfo::find().postalAddress().CountryRegionId;
            this.createOrUpdatePriceGroup();

            if (this.RefCompanyId       == dataArea.Id ||
                this.CountryRegionId    == countryRegionId ||
               (this.RefCompanyId == "" && this.CountryRegionId == "" && this.CurrencyCode == ledger.AccountingCurrency))
           {
                if (InventTable::exist(this.ProductNumber))
                {
                    priceDiscAdmName = this.findOrCreatePriceDiscAdmName(#ImportPriceDiscJournalName,"@SYS342509");

                    priceDiscAdmTable.clear();
                    priceDiscAdmTable.JournalName       = priceDiscAdmName.JournalName;
                    priceDiscAdmTable.Name              = priceDiscAdmName.Name;
                    priceDiscAdmTable.insert();

                    priceDiscAdmTrans.clear();
                    priceDiscAdmTrans.initValue();
                    inventDimId = InventDim::findOrCreateBlank().InventDimId;

                    while select firstonly priceDiscTable
                        where   priceDiscTable.Relation         == PriceType::PriceSales
                            &&  priceDiscTable.ItemCode         == TableGroupAll::Table
                            &&  priceDiscTable.ItemRelation     == this.ProductNumber
                            &&  priceDiscTable.AccountCode      == TableGroupAll::GroupId
                            &&  priceDiscTable.AccountRelation  == #RRP
                            &&  priceDiscTable.Currency         == this.CurrencyCode
                            &&  priceDiscTable.InventDimId      == inventDimId
                            &&  priceDiscTable.PriceUnit        == this.PriceUnit
                    {
                        priceDiscAdmTrans.initFromPriceDiscTable(priceDiscTable);
                    }
                    priceDiscAdmTrans.Relation         = PriceType::PriceSales;
                    priceDiscAdmTrans.ItemCode         = TableGroupAll::Table;
                    priceDiscAdmTrans.ItemRelation     = this.ProductNumber;
                    priceDiscAdmTrans.AccountCode      = TableGroupAll::GroupId;
                    priceDiscAdmTrans.AccountRelation  = #RRP;
                    priceDiscAdmTrans.Currency         = this.CurrencyCode;
                    priceDiscAdmTrans.InventDimId      = inventDimId;
                    priceDiscAdmTrans.PriceUnit        = this.PriceUnit;
                    priceDiscAdmTrans.JournalNum       = priceDiscAdmTable.JournalNum;
                    priceDiscAdmTrans.QuantityAmountFrom  = 0;
                    priceDiscAdmTrans.Amount           = this.Amount;
                    priceDiscAdmTrans.UnitId           = InventTable::find(this.ProductNumber).salesUnitId();
                    priceDiscAdmTrans.SearchAgain      = NoYes::Yes;
                    priceDiscAdmTrans.insert();
                    if(priceDiscAdmTable.RecId)
                    {
                        priceDiscAdmCheckPost = new PriceDiscAdmCheckPost(false);
                        priceDiscAdmCheckPost.initJournalNum(priceDiscAdmTable.JournalNum);
                        priceDiscAdmCheckPost.run();
                        infolog.clear();
                    }
                }
            }
        }
    }
    ttscommit;
}

Generic prices in Dynamics AX 2013 R3

In Dynamics AX 2012 R3 you can specify a sales price on a released item. You cannot specify a price on a set of products using the trade agreements.

I wanted to change this, so that AX 2012 R3 do have the possibility to have a price that is used by many released products. Here is what I did:

First I created a new field on the item master sales (InventTableModule):

Then I made it possible to create sales price groups, and to specify prices per currency for this price group:

Then when creating sales order it selects the generic price.

With this little change I can now control prices on thousands of released products, and rather to put individual prices on each item, I just classify each item to a price group. Let’s say I have 10.000 released products and prices in 6 currencies this would minimum have resulted in 60.000 records in the price agreement table. If I in addition have 10 companies where I also want to use the prices, this results in 600.000 records on prices.

If I have standardized my pricing levels to consumer oriented prices, and using the sales price group, I would only need to maintain 60 * number of price groups. Just a fraction J

To make this happen, the following objects needs to be modified:

If you come across a similar requirement or you need this, feel free to contact me on the blog J

Happy DAX’ing

AX2012 : Use queries as criteria

In Dynamics AX 2012 the relation between different records is often defined as a “table-group-all” relation. One example of this is the setup of supplementary items, where you can define the relation as a Table-Group-All relation.

image

Even though this approach is easy to use and understand, it does have limitations.  Often you would like to pin-point the relationships even further.  It can be based on other fields in the source record.

I wanted to look against another approach, where I can use a query as a criteria.  Normally queries are used to fetch specific records based on a set of ranges and defined sorting criteria.

I have a label software, that integrates Dynamics AX 2012 with BarTender, and it is generic, meaning that it can produce labels based on all data sources in AX.

image

So in this case, I can setup that when the user wants to print a label, I can search through a set of label templates, and check this record if it fits the query criteria.  I then have a endless possibility to specify all kinds of criteria to select the best matching label template.

The trick is to add the source recid as a range to the packed query criteria exactly when checking for it.  Also to add the “firstonly” hint, so that you don’t query for more than you want.

Here is some sample job, I used to test out this approach:

image

Microsoft is also using the same approach when selecting is a WMSorder can be added to a existing shipment.  Take a look at  WMSShipment.canWMSOrderBeAdded() or at wmsShipmentTemplate.canWMSOrderBeAdded(), then you see the same pattern.

I hope you see the value in this, and that it can open a complete new way of adding rules and relational information to Dynamics AX 2012.  With this approach we can generically pinpoint our criteria for our rules.

Take care and happy DAXing 

Reservation sequence in AX 2012

I have received some questions on how does Dynamics AX 2012 decide what on-hand to select when reserving. Some thinks it is the inventory model (FIFO etc), but it is not. Some times it can seam like AX 2012 has it’s own will can selects randomly.

image

If the same item is located in many different buffer locations, there is a rule. It all comes down so sorting of inventory dimensions, that is specified in the InventoryDimensions group on the table InventDim.

image

So this means that the Inventsum is sorted on these inventory dimensions alphanumeric ascending. Meaning that items on location A001 will be reserved before A002 if available physical.

If you dig deep into the following method you find the code (InventSum::newQueryReservation()):

image

image

image

There are ISV solutions that addresses this, and that solves this in a very elegant way.  But now you know where to look if you want to challenge reservation sequence in AX 2012 with a good old customization.

(There are FEFO reservation sequences in AX 2012 but this is not addressed here)

Happy Daxing

Dynamics AX 2012 architecture for carton, packages and pallets

When shipping goods, there are many requirements that needs to be fulfilled. You have the ability to have correct addresses, customer requirements, freight forwarder requirements and legal requirements. To solve this we have many different “systems” in Dynamics AX 2012 (and earlier versions) to solve each of these requirements. Some of these systems are:

  • Sales Shipping Stats – Print the std. AX delivery label, and get volumetric information on the packingslip/invocie.
  • Bill of Lading – A document used with the freight forwarder to keep track of where to deliver the goods.
  • Packing material – A system to keep track of fees related to packing material
  • Ship carrier integration – A system for integrating to shipping software, like UPS Worldship og Clipper ship. Can also be used in „test mode“ if you don’t have such a shipping software.

I have gone as deep as possible into the subject to understand, and my judgment is “The data model for storing and maintaining package information in Dynamics AX 2012 is a mess!” In reality we have more than 4 different systems for maintaining package information. In reality this means we could have 4 versions of the truth. And to cover all eventualities and requirements of a advanced distribution company we need to make sure that we update all the necessary information in all 4 different systems. If Microsoft is reading this post, my suggestion for the next release is to combine these systems into one.

image

But it can take several years before our customers could benefit from such a change. So this blog post is about how we can make this work with what we have. First I will go deep on each of these 4 “systems”.

Sales shipping stats

The first “system” is the Shipping Specification. These are fields you register when you do packing slips.

image

Here you can also select to print out the standard AX delivery label.

image

The design is not very impressive and it clearly needs some customizations to make this acceptable for any customer of freight forwarder. But I guess that the idea from Microsoft is that shipment labels is not printed from Dynamics AX, but from a shipping software.

There also exists an inquiry form, where you can reprint the standard Dynamics AX 2012 delivery labels. I have found this menuitem available in the main manu under sales –> inqueries –> distribution –> Shipments. (Quite confusing since this is a terminology used for WMSShipments)

image

After the packingslip is posted, the user cannot change the data here.

The information of this is stored in a table called SalesShippingStat.

image

The “filling” of this information is manually done before doing a packing slip update, but it gets “marked” with Packingslip ID in the following code.

image

We see from this code, that the Sales Shipping Stat system does not have any uniqueness on the data or cartons. This code just related to get the summary of weight and cartons on the packingslip. Also see that the SalesParameters.useShippingStatOnPackingslip must be enabled for it to work.

Bill of Lading

The bill of lading is a receipt for goods that the carrier agrees to transport and to deliver to a designated person. It shows the list of goods delivered for transportation. In case of loss, damage, or delay, the bill of lading is the basis for filing freight claims. The bill of lading includes information about:

  • The customer order.
  • The carrier.
  • The origin and destination of the shipment.
  • The number of units in the shipment.
  • Freight charge terms.
  • Special instructions.

The bill of lading system is originating from the shipping module, and do not share any information with the SalesShippingStat system. It makes it possible to create a formletter like this :

image

The bill of lading can be both automatically and manually created,

image

The Bill of lading system is much more comprehensive than the sales shipping stat system, because it is possible to have some kind of unique ness.

The tables and fields used in this system is the following:

image

As seen here, the tables are missing the WMSPickPalletId or any SSCC-capable fields.

The tables refers to the following :

image

The automatic creation of BOL’s are done in the WMSShipment.ship method:

image

image

What I would expect from the BOL, is that the uniqueness of each package/pallet would be more clear. In a consolidated picking scenario we do have the WMSPickPalletID, that could provide this uniqueness, but this information is never passed into the bill of lading tables.

There is therefore no way to pass information like SSCC etc, or to use the BOL system as a basis for ASN/EDI processes. It is all about creating the Bill-Of-Lading document.

Packing material

The third way of dealing with packing material is the Packing material tables

image

The packing material is primarily for calculating a packing material fee. The table is normally filled in when a sales invoice is posted.

The tables look like this:

image

The packing material system therefore seems to only have one single function. To keep track of packing material fees, and not for logistical purposes.

Shipping carrier integration tables.

The shipping carrier interface feature allows Microsoft Dynamics AX customers to integrate with shipping software. The shipping software packages can pull information from Microsoft Dynamics AX, process packages, and then move their information into Microsoft Dynamics AX, eliminating manual entry and improving tracking visibility.

In the Shipping Carrier Interface for Dynamics AX 2009, Microsoft described this feature as this in the following whitepaper.

image

It is intended that the packing is handled in an external shipping, and then later transferred to Dynamics AX. This is described as a process that happens between packing slip and invoicing.

It is possible to manually fill in the information, by placing the carrier integration in “test-mode”. When posting the packing slip update, then the following screen will appear:

image

If we look deeper into the implementation of this ship carrier system, we see that it is interesting, because it does contain much what is needed. I would say that it is the best “survival candidate” of the 4 different systems.

What happens, when the packingslip is posted is the following:

image

PS! Check out the calculation of the ShipCarrierCODPackage.codAmount. It does not cover discounts!

The fields in the requester table contains the following:

image

It is a bit sad, that it does not transfer any information about already created pick pallets into the ShipCarrierShippingRequest. In a consolidated picking scenario often these pick pallets have already been created, and it would simply the process of that was available for the shipping software.

It is in the process made so that the shipping software will return data into a staging table, that contains the following fields.

image

What is interesting here, is that the PackageId field, that is actually extended from WMSPickPalletId. So this means that pallet ID can be returned back. What is missing is information about width, height, length and volume. But Microsoft have placed this information in the table ShipCarrierCODPackage:

image

But this volumetric information is never filled in by standard AX. The table is only created from AX if there is a COD(Cash On Delivery).

A solution suggestion

There are some facts. We have AX 2012 and AX 2014 is not here yet. Our customers hate large footprint customizations, because they make upgrades more difficult. And there is already many dependencies in AX 2012 that we need to take care of. So what do I do for my customers ? I cheat the system.

  1. When picking all picking transactions will be marked with a pick pallet ID.
  2. When Packing I have a new table, that is quite large and „flat“, but contains almost all I need to fiels for my labels, freight charges, weight, volumetric information ++++
  3. After packing is done per shipment, then I will generate information’s into all the 4 systems from the „New architecture“ system, and prevent the standard AX to generate this information.

image

This gives me much more control, and also the ability to have uniqueness on all outbound cargos, tracking +++. And my footprint is ZERO in standard AX, and compatible with my PDA and label printing system (BTI)

Get product attributes from X++

The product attributes is a nice feature, where we can add attributes to the products without adding any additional fields on to the inventory table.

image

But I wanted to be able to fetch out only the attribute names and values from X++, that had values.

image

Here is how I solved it.


static void Demo_GetProductAttributes(Args _args)
{
    inventTable                 InventTable;
    EcoResProductAttributeValue ecoResProductAttributeValue;
    EcoResAttribute             ecoResAttribute;
    EcoResValue                 ecoResValue;

    while select InventTable where InventTable.itemid == "1604"
        join RecId from ecoResProductAttributeValue
        where ecoResProductAttributeValue.Product == InventTable.Product
            join Name from ecoResAttribute
            where ecoResProductAttributeValue.Attribute == ecoResAttribute.RecId
                join ecoResValue
                where ecoResValue.RecId == ecoResProductAttributeValue.Value
    {
        info(strFmt("%1 - %2 - %3", InventTable.ItemId, ecoResAttribute.Name, ecoResValue.value()));
    }
}

AX2012 FP Product receipt from X++

Here the other day I have seen a change in functionallity between AX 2012 (6.0) and AX 2012 Feature Pack (6.1).  At several customers we are using simplified product receipt, where the warehouse worker and do a product receipt from a small PDA screen, after the items has been registrered :

image

A small demo X++ code for doing the product receipt is like this (for AX 2009 and AX 2012):

image