Thursday, September 2, 2021

LCS Downloadable Tools

Ok..Ok..I've lost this page a number of times.  The downloadable tool page in LCS is:

https://lcs.dynamics.com/V2/LocalTools/

This page has all of the available tools for download, include IDMF, Data Import/Export and the Security Development tool.

Thursday, May 13, 2021

D365 for F&O: An Explanation of Ledger and Default Dimensions with Code

Another blogger finally illustrated the differences between Default Dimensions and Ledger Dimensions.  Quick Note: Default Dimensions are stored in DimensionAttributeValueSet and Ledger Dimensions are stored in DimensionAttributeValueCombinate.  Code to create a new dimension of each type is below.

Dimensions

For reference, the containers are an array of dimension attribute names/values in the order expected for the dimension:

container conAttr = ["MainAccount", "Division", "Department", "CostCentre", "Program"];

container conValue = [this.MainAccountId, this.BusinessUnit, this.Department, this.CostCenter, this.Program];

 //This method creates a default dimension in DimensionAttributeValueSet

    public static DimensionDefault createNewDefaultDimension(container _conAttr, container _conValue)

    {

        DimensionAttributeValueSetStorage   valueSetStorage = new DimensionAttributeValueSetStorage();

        DimensionDefault                    result;

        int                                 i;

        DimensionAttribute                  dimensionAttribute;

        DimensionAttributeValue             dimensionAttributeValue;


        str                             dimValue;

    

        for (i = 1; i <= conLen(_conAttr); i++)

        {

            dimensionAttribute = dimensionAttribute::findByName(conPeek(_conAttr,i));

            if (dimensionAttribute.RecId == 0)

            {

                continue;

            }

            dimValue = conPeek(_conValue,i);

            if (dimValue != "")

            {

                dimensionAttributeValue = dimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute,dimValue,false,true);

                valueSetStorage.addItem(dimensionAttributeValue);

            }

        }

    

        result = valueSetStorage.save();


        return result;

    }


    //This method creates a ledger dimension found in DimensionAttributeValueCombination

    //Assumption: MainAccountId is expected in the container _conData

    public static LedgerRecId createNewLedgerDimension(MainAccountNum _mainAccountId, container _conValue)

    {

        int                                             hierarchyCount;

        int                                             hierarchyIdx;

        RecId                                           dimAttId_MainAccount;

        LedgerRecId                                     ledgerRecId;

        MainAccount                                     mainAccount;

        RefRecId                                        recordvalue;

        DimensionAttribute                              dimensionAttribute;

        DimensionAttributeValue                         dimensionAttributeValue;

        DimensionSetSegmentName                         dimensionSet;

        DimensionStorage                                dimStorage;

        LedgerAccountContract ledgerAccountContract =   new LedgerAccountContract();

        DimensionAttributeValueContract                 valueContract;

        List                                            valueContracts = new List(Types::Class);


        dimensionAttributeValueCombination dimensionAttributeValueCombination;


        mainAccount = MainAccount::findByMainAccountId(_mainAccountId);

        recordvalue = DimensionHierarchy::getAccountStructure(mainAccount.RecId,Ledger::current());

        hierarchyCount = DimensionHierarchy::getLevelCount(recordvalue);

        dimensionSet = DimensionHierarchyLevel::getDimensionHierarchyLevelNames(recordvalue);


        for(hierarchyIdx = 1; hierarchyIdx <= hierarchyCount; hierarchyIdx++)


        {

            if(hierarchyIdx == 1) //Main Account

                continue;


            dimensionAttribute = DimensionAttribute::findByLocalizedName(dimensionSet[hierarchyIdx],false,"en-us");


            if(dimensionAttribute)

            {

                dimensionAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute, conPeek(_conValue,hierarchyIdx));


                if(dimensionAttributeValue)

                {

                    valueContract = new DimensionAttributeValueContract();

                    valueContract.parmName(dimensionAttribute.Name) ;

                    valueContract.parmValue(dimensionAttributeValue.CachedDisplayValue);

                    valueContracts.addEnd(ValueContract);

                }

            }

        }


        ledgerAccountContract.parmMainAccount(_mainAccountId);

        ledgerAccountContract.parmValues(valueContracts);

        dimStorage = DimensionServiceProvider::buildDimensionStorageForLedgerAccount(LedgerAccountContract);

        dimensionAttributeValueCombination = DimensionAttributeValueCombination::find(dimStorage.save());

        ledgerRecId = dimensionAttributeValueCombination.RecId;


        return ledgerRecId;


    }


Thursday, May 7, 2020

QR Codes in Ax 2012 and C#

I had a request over the last couple of days to add  a QR Code to 2 SSRS reports: 1 in Ax 2012 and another in a WPF application written in C#.  QR Codes are of often thought as a font and they are somewhat analogous.  However, it was very clear very early that QR Codes are images.

In X++, QR codes can be created in a container.  The container can be an field on a table.  Once it's populated, you place it on your SSRS report and it displays.  It is very similar with D365.  To keep things easy, I wrote the QR Code Generation into a static method in a class.

server static container createQRCode(Str1260 qrString)
{
    Microsoft.Dynamics.QRCode.Encoder   qrCode;
    System.String                       netString;
    str                                 tempFileName;
    System.Drawing.Bitmap               netBitmap;
    FileIOPermission                    perm;
    BinData                             binData;
    container                           result;
    ;

    netString = qrString;
    qrCode = new Microsoft.Dynamics.QRCode.Encoder();
    qrCode.set_Version(10);
    netBitmap = qrCode.Encode(netString); //encode the string as Bitmap can be used already

    tempFileName = qrCode.GetTempFile(netString); //to get the QR temporary file

    perm = new FileIOPermission(tempFileName,'r');
    perm.assert();
    binData = new binData();
    binData.loadFile(tempFileName);
 //get the QR code image put inside container so can be stored inside database for storing or reporting purpose
    result = binData.getData();
    System.IO.File::Delete(tempFileName);

    CodeAccessPermission::revertAssert();

    return result;
}

In the WPF application, I decided to install a 3rd party component: MessagingToolkit.QRCode.  I found it on NuGet at https://www.nuget.org/packages/MessagingToolkit.QRCode.  Although it is a bit old, it worked perfectly.  The only trick is that SSRS does not recognize Image objects coming from .Net, rather its image widget needs to be populated with a Byte array.  However, once that hurdle is figured out, the rest was rather easy.

using MessagingToolkit.QRCode.Codec;
...

public Byte[] QRCode
        {
            get
            {
                QRCodeEncoder encoder = new QRCodeEncoder();
                encoder.QRCodeVersion = 10; //higher versions allow larger QR Codes.
                encoder.QRCodeErrorCorrect = QRCodeEncoder.ERROR_CORRECTION.M;

                string qrText = this._ticket.Number + ", " + this._ticket.NetWeight.ToString();
                Bitmap qrCode = encoder.Encode(qrText, new UTF8Encoding());
                MemoryStream imageStream = new System.IO.MemoryStream();
                qrCode.Save(imageStream, System.Drawing.Imaging.ImageFormat.Png);

                return imageStream.GetBuffer();
            }
        }

One thing to notice: the MS component, save the QR Code to a temp file and then loads it into byte array (BinData).  The MessagingToolkit is nicer as the operation appears to be done entirely in memory.

Friday, November 8, 2019

WCF Web Service Receiving Null Parameter

I was recently working with a client application (WPF) that used WCF Web Services to query and post data to Dynamics Ax (2012). One the requirements needed a new web service to be deployed.  I once again reached one of those situations where the service worked in development and it passed testing with no issues.  However, in 2 of roughly 10 sites, it started failing. One of my associates started debugging and found that the client application was sending the data, however the web service was receiving a null parameter.  I should mention that the parameter was being sent as content data in a post request in json format.

Getting copy of the data being sent was not much of problem.  I then emulated the call using Postman.  Irritatingly, it worked. So, it works in Postman and does not work in the client application and we were almost stumped.  As a last ditch attempt, I rewrote the sending routine to iterate through the records and send 5 records to be processed only.  The routine failed on the second iteration.  Debugging the call and extracting the data, I found that there was a French character in the stream.  Rechecking the settings on Postman, I found that the Postman was using UTF-8.  Honestly, I thought this would be default these days, but I guess I was mistaken.  I modified the call to force it to use UTF-8 and it worked.

For reference:
using (WebClient client = new WebClient())
{
     client.Headers[HttpRequestHeader.ContentType] = "application/json";
     client.Encoding = Encoding.UTF8;

     …<do other stuff>
     var dataString = client.UploadString(apiUrl, jsonObj);
     ResponseClass webresult = JsonConvert.DeserializeObject<ResponseClass>(dataString);
}

Tuesday, June 25, 2019

WPF: Validation and Dependency Objects

I recently had to do some support work for a WPF application.  I had not used WPF or MVVM in several years, so it was a nice change.  However, one of the requirements was to create a range validator and attach it to textbox.  The architecture of this part tied the View to the base class of a View Model that had several derived classes.  Each derived class had different minimum and maximum boundaries that needed to be reflected when the view was opened.

There were a couple of very good treatments that I found.  The process is as follows:
  1.  Create the Min and Max Properties in the View Model.
  2.  Create a class derived from ValdationRule.
  3.  Create a wrapper class derived from DependencyObject.  The Dependency Properties will represent the min and max that you need.
  4.  Add a property of the wrapper class in the Validation Rule.
  5.  Create a class derived from Freezable that will enable the View to access the DataContext and pass values to the wrapper.
  6.  Add the elements to the View.  If done properly, the Min and Max needed by the wrapper can be set to values defined in the ViewModel.

Tuesday, March 19, 2019

Ledger Dimension

I finally learned...A basic dimension is a default dimension.  This is a value that concatenates a number of dimension values and is located in the Dimension Attribute Value Combination Table.  If you add a Main Account, then you have a Ledger Dimension.  The combination that you may try to retrieve or create MUST be part of the Main Account's Account Structure.

There are 2 approaches to getting a Ledger Dimension
1) Query it with AxdDimensionUtil::getLedgerAccountId
2) Create or retrieve it with DimensionDefaultingService::serviceCreateLedgerDimension

The code to retrieve a Ledger Dimension is:

private DimensionDefault getLedgerDimension(MainAccountNum _mainAccount, Num, str _bu, str _costcenter, str _dept)
{
    DimensionDefault result = 0;
    int position = 0;
    int dimensionCount = 0;
    container account;

    account += [_mainAccountNum, _mainAccountNum, 0];
    if(_bu)
    {
        account += ['BusinessUnit', _bu];
        dimensionCount++;
    }
    if(_dept)
    {
        account += ['Department', _dept];
        dimensionCount++;
    }
    if(_costcenter)
    {
        account += ['CostCenter', _costcenter];
        dimensionCount++;
    }

    account = conPoke(account, 3, dimensionCount);
    try
    {
        result = AxdDimensionUtil::getLedgerAccountId(account);
    }
    catch
    {
        return 0;
    }

    return result;
}


The code to called serviceCreateLedgerDimension requires a Main Account Number and Default Dimension:

private DimensionDefault getLedgerDimensionFromDefaultDimension(MainAccountNum _mainAccountNum, DimensionDefault _defaultDimension)
{
    DimensionDefault result = 0;
    DimensionDefault ledgerDimension;

    container account = [_mainAccountNum,_mainAccountNum, 0];
    ledgerDimension = AxdDimensionUtil::getLedgerAccountId(account);
    result = DimensionDefaultingService::serviceCreateLedgerDimension(ledgerDimension, _defaultDimension);

    return result;
}

Tuesday, March 12, 2019

D365 FO and External DLLs

We recently had to add some external dlls to our D365 for Finance and Operations model.  We found out that when the references are added to a project, an XML descriptor of the reference is placed in $\LCS Dynamics Ax\Trunk\<your branch>\Metadata\<your model>\<your model>\AxReference.  However, once you ready to deploy, the actual dll must be manually added to $\LCS Dynamics Ax\Trunk\<your branch>\Metadata\Bin.  Otherwise, the LCS build will fail with a class not found error.