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.

Friday, February 8, 2019

Positive Pay in D365 For Finance and Operations for 8.1

One of my clients recently upgrade to D365 version 8.1.  This was a Microsoft mandated change as this is the one where overlays will no longer be allowed.  After the dust settled, I was asked to make some changes to their Positive Pay files.  The process worked in their previous environment, however under 8.1 the documents were being produced without data.

After debugging the process, I was able to isolate the issue to the MS side of code.  Specifically, the DMF process was not able to find the required records.  I raised a ticket with MS and they resolved it in short order.  There appears to be a breaking change in 8.1 there the entity name must be capitalized in the XSLT file.

So instead of the old:

Document/BankPositivePayExportEntity

You need to put:
Document/BANKPOSITIVEPAYEXPORTENTITY

The documentation from MS has not been updated  yet.

Wednesday, November 28, 2018

File Uploads in Dynamics 365 For Operations

I was recently reviewing some file import methods and I came across a new (for me) method of uploading files from a client.  It turns out this is quite simple.

This calls a file finding dialog:
FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();

This returns a System.IO.Stream:
System.IO.Stream stream = uploadResult.openResult();

These snippets were part of class that imported a 2 column spreadsheet in Excel.  Please note that X++ now supports using statements. Here is the full code for reference:

using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using OfficeOpenXml.Table;

public class PayrollCodeImport
{
    public static void main(Args _args)
    {
        System.String value = null;
        OfficeOpenXml.ExcelRange cell = null;
        PayrollCode payrollCode;
        //upload
        FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();
        PayrollCodeId payrollCodeId;
        Description  description;

        if(!uploadResult)
        {
            return;
        }
        //construct a new spreadsheet document with the upload stream
        OfficeOpenXml.ExcelPackage package = new ExcelPackage(uploadResult.openResult());
        //OXMLSpreadsheetDocument_RU spreadsheet = OXMLSpreadsheetDocument_RU::constructFromStream(uploadResult.openResult());
        //get the worksheets from the workbook
        OfficeOpenXml.ExcelWorksheets worksheets = package.get_Workbook().get_Worksheets();
        //get the worksheet by name
        OfficeOpenXml.ExcelWorksheet payrollcodeWorksheet = worksheets.get_Item("Import") as OfficeOpenXml.ExcelWorksheet;
        //skip the header line
        for (int i = payrollcodeWorksheet.Dimension.Start.Row + 1; i <= payrollcodeWorksheet.Dimension.End.Row; i++)
        {
            value = null;
            cell = payrollcodeWorksheet.Cells.get_Item(i, 1);
            value = cell.Value;
            payrollCodeId = value;

            value = null;
            cell = payrollcodeWorksheet.Cells.get_Item(i, 2);
            value = cell.Value;
            description = value;

            if(payrollCodeId && description)
            {
                payrollCode.Clear();

                select forupdate payrollCode where payrollCode.PayrollCodeId == payrollCodeId;
                ttsbegin;
                if(!payrollCode.RecId)
                {
                    payrollCode.PayrollCodeId = payrollCodeId;
                    payrollCode.Description = Description;
                    payrollCode.insert();
                }

                ttscommit;
            }
        }
    }

}

Friday, August 24, 2018

Dynamics 365 FO Connection to ServiceBus

I recently received a request to connect D365 FO to an Azure ServiceBus in order to download messages that were being sent by another application.  During the course of development, I found that there were 2 sets of projects that were developing connecting pieces: Microsoft.Azure.ServiceBus and Microsoft.ServiceBus.  The first component, however, was tied to .Net 4.6.1 and D365 FO is tied to .Net 4.5.2.  So, using the Azure component was out of the question.  However there was no documentation on how to use the object model in the straight ServiceBus.  Fortunately, both projects use a similar approach and a similar object model that I was able to make adaptations.  There were 2 caveats.


  1. D365 does not appear to work well with .Net async approaches.
  2. D365 does not appear to work well with generics.
In order to get around this, I used a synchronous approach and passed any element that required using generics to a C# class that could do the processing. The Service Bus dll can be downloaded from https://www.nuget.org/packages/WindowsAzure.ServiceBus/4.1.11

Once downloaded, open it as a zip file (you may need to change the extension to zip) and extract out the dll.  Then add the dll as a reference in D365.  Once there, the following class can be used to connect and download messages.  Sending messages would use a similar logic.  Please note that my requirements were to use Topics and Subscriptions rather than Queues.  Also, please note that I save all of the connection parameters in a separate table.  That way the system may access different Topic/Subscription combinations as required.  Also, if the connection information is changed, then this can be reflected in data.

public class ServiceBusConnect
{
    public void new()
    {  
    }

    public void receiveMessages(str _topic, str _subscription)
    {

        str strBody;
        System.IO.Stream stream;
        System.IO.StreamReader reader;
        Microsoft.ServiceBus.Messaging.SubscriptionClient subscriptionClient;
        Microsoft.ServiceBus.Messaging.MessagingFactory messagingFactory;
        Microsoft.ServiceBus.Messaging.BrokeredMessage message;
        ServiceBusConnectionParameters parameters;
       
        select firstonly parameters where parameters.Topic == _topic && parameters.Subscription == _subscription;
        ServiceBusConnector.BrokeredMessageProcessor connector = new ServiceBusConnector.BrokeredMessageProcessor();
        messagingFactory = Microsoft.ServiceBus.Messaging.MessagingFactory::Create(parameters.ConnectionString,
                Microsoft.ServiceBus.TokenProvider::CreateSharedAccessSignatureTokenProvider(parameters.SharedAccessKeyName, parameters.SharedAccessKey));

        subscriptionClient = messagingFactory.CreateSubscriptionClient(parameters.Topic, parameters.Subscription);
        message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
        if(message != null)
            setPrefix("Messages");

        while(message != null)
        {
            strBody = connector.GetBrokeredMessageContents(message);
            info(strBody);
            subscriptionClient.Complete(message.LockToken);
            message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
        }
        subscriptionClient.Close();
    }

}

The table ServiceBusConnectionParameters contains all the data for connection to Service Bus.  The Topic and Subscription should be based on how your project's service is configured.  The connection string will be sb://<your project>.servicebus.windows.net.  The SharedAccessKeyName will be what ever is configured in your service bus.  The default is RootManageSharedAccessKey.  The shared access key is the primary key as displayed on your Service Bus Explorer.

As usual for using .net assemblies in D365, all class references must be fully qualified with their name spaces.  The parameter in the subscriptionClient.Receive call is a timeout.  If no messages are found within 5 seconds, the call is aborted and the message is set to null.  Without it, the system just hangs indefinitely waiting for messages.  The call to GetBrokeredMessageContents uses a call to the GetBody method on the message itselft.  This method requires a generic reference to type the message.  I could not, however, find a way to do that within D365.  The wrapper to deserialize the message was pretty straightforward.

using System.IO;
using Microsoft.ServiceBus.Messaging;


namespace ServiceBusConnector
{
    public class BrokeredMessageProcessor
    {
        public string GetBrokeredMessageContents(BrokeredMessage message)
        {
            string strResult;

            using (var stream = message.GetBody<Stream>())
            {
                using (var reader = new StreamReader(stream))
                {
                    strResult = reader.ReadToEnd();
                }
            }

            return strResult;
        }
    }
}



Tuesday, June 5, 2018

Ax 2009 - Payment Journal Report Slow

We have a client on Ax 2009 and was saying that the Payment Journal Report was slow (not completing).  One person in my group has seen the issue before.  Her solution was:

  1. Stop AOS
  2. Delete CustVendPaymReport from SSRS
  3. Restart SSRS service
  4. Backup and delete XPPIL file contents
  5. Start AOS
  6. Redeploy custVendPaymReport
  7. Run Full CIL
  8. Run DB sync
I'm not sure why this works.  My colleague seems to think it's because the report has an uncommitted transaction.  However, if that were the case, restarting SQL Server would have solved it.  Also, it does not explain why redeploying the report is part of the process.

I have 2 suspicions, but can't prove either.  One is that the queries involved in gathering the data is missing a needed index.  We tried running the Ola scripts to evaluate and rebuild statistics or reorg/rebuild indexes.  That did not help.  The other possibility is that somehow one of the db temp tables is surviving from run to run when it should not.

Dynamics 365 SSL Certificates Expire (HTTP 500)


I had a major issue today with my Impark local development environment.  Whenever, I tried to start an instance, I kept getting an HTTP 500 (internal Server Error).  Checking HTTP 500 with D365 got me nowhere.  I finally looked in my Event Viewer, I found a number of errors related to Expired Certificates.



Exception information:

    Exception type: ExpiredCertificateException

    Exception message: Expired certificate for id '61EF4024783735137DBA51E341E36A0AE6A89497'.



Checking that, I was able to find that the SSL Certificates used by D365 had expired.  It appears that the copy of my installation is a year old.  Checking further, I was able to find a way to renew the certificates.    Search and Open Manage Computer Certificates.






Select Personal -> Certificates.  Look For the DeploymentOneBox entries.


 

For each, double click and go Details -> Thumbprint.


 

Copy and Paste the Thumbprint to Notepad. Put each entry on a separate line.  When finished, replace all spaces with empty.  Once you have all 4 numbers, you can create a power shell script.  At the top of file, put:

Set-Location -Path "cert:\LocalMachine\My"



The for each Thumbprint, create the following 2 lines:

$OldCert = (Get-ChildItem -Path 2e10fea6fcd1a23d0ba5dfefe5ce2eb3d8915f1a)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)



Where the long number is the Thumbprint.



The full script should look like:



Set-Location -Path "cert:\LocalMachine\My"



$OldCert = (Get-ChildItem -Path 2e10fea6fcd1a23d0ba5dfefe5ce2eb3d8915f1a)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)



?$OldCert = (Get-ChildItem -Path 61ef4024783735137dba51e341e36a0ae6a89497)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)



?$OldCert = (Get-ChildItem -Path 3591d2eb68e980666190fe2b4c7f2abf14e55871)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)



?$OldCert = (Get-ChildItem -Path cc5ba410b7fbb04249020b181fdad0fcf999698a)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)





You can run all the thumb prints one at a time or all together.  After each pair, you will get some output.  Copy and paste the new Thumbprint under the entry for the old one.  Your file should now look like:



Set-Location -Path "cert:\LocalMachine\My"



$OldCert = (Get-ChildItem -Path 2e10fea6fcd1a23d0ba5dfefe5ce2eb3d8915f1a)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)

B977B19D03EDB88B4D5CD647DFA839D3E4AEBA23



?$OldCert = (Get-ChildItem -Path 61ef4024783735137dba51e341e36a0ae6a89497)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)

47CF0D5220C9457ABEFB53AF026600ED24A00584



?$OldCert = (Get-ChildItem -Path 3591d2eb68e980666190fe2b4c7f2abf14e55871)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)

1F710D2AE32F0202499C586F3A1B8B3A6FE1EC7A



?$OldCert = (Get-ChildItem -Path cc5ba410b7fbb04249020b181fdad0fcf999698a)

New-SelfSignedCertificate -CloneCert $OldCert -NotAfter (Get-Date).AddMonths(999)

9256AC02204C006D47AC10D1DD51DC27DF3B2A6F



Where the number under each pair is the new thumbprint.  Now in Visual Studio, open <D365 Install Drive>:\ C:\AOSService\webroot\web.config, wif.config and wif.services.config.  Once open, do a global replace in files of the Old Thumbprint with the new Thumbprint.  Save the files with a .config.new extension.  Open the file explorer and navigate to the webroot directory.  Take the existing config files and add the extension .old or .bak.  Remove the .new extensions from the 3 new files (this can’t be done in Visual Studio owing to permission issues).  Once complete, reboot and you should be working again.

Tuesday, May 1, 2018

Screen Resolution Issues

Recently, I was working for a new client.  When I logged into Ax, the font sizes seemed uncomfortably small.  The major problem, however, was that an SSRS report that I was working on had major presentation issues (fields cut off, sections not appearing, etc.).  When I exported the report to PDF (for once), the presentation was fine.  So, I decided to see if I could adjust the resolution of Ax to see if that was the problem.  Turned out it was.  Here is the link:

https://stoneridgesoftware.com/dynamics-ax-2012-high-resolution-screens-bad-relationship/

Wednesday, April 25, 2018

Financial Dimensions....Pesky Financial Dimensions

Finally...in code, how to retrieve a dimension value from a default dimension and a dimension name:

public str getDimensionValueByName(DimensionDefault _dimension, str _dimensionName)
{
    DimensionAttributeValueSetStorage dimStorage;
    Counter i;
    str result;
   
    dimStorage = DimensionAttributeValueSetStorage::find(_dimension);
    for(i = 1; i <= dimStorage.elements(); i++)
    {
        if(DimensionAttribute::find(dimStorage.getAttributeByIndex(i)).Name == _dimensionName)
        {
            result = dimStorage.getDisplayValueByIndex(i);
            break;
        }
    }
   
    return result;
}

How to get or create a default dimension:

public DimensionDefault getOrSetDefaultDimension()
{
    DimensionAttributeValueSetStorage   valueSetStorage = new DimensionAttributeValueSetStorage();
    DimensionDefault result;
 
    int i;
    DimensionAttribute dimensionAttribute;
    DimensionAttributeValue dimensionAttributeValue;
    str dimValue;  
 
    container conAttr = [d1Name, d2Name, d3Name];
    container conValue = [d1Value, d2Value, d3Value];
 
    for (i = 1; i <= conLen(conAttr); i++)
    {            
        dimensionAttribute = dimensionAttribute::findByName(conPeek(conAttr,i));
     
        if (dimensionAttribute.RecId == 0)
        {
            continue;
        }
     
        dimValue = conPeek(conValue,i);
     
        if (dimValue != "")
        {
            // The last parameter is "true". A dimensionAttributeValue record will be created if not found.
            dimensionAttributeValue =
                    dimensionAttributeValue::findByDimensionAttributeAndValue(dimensionAttribute,dimValue,false,true);
         
            // Add the dimensionAttibuteValue to the default dimension
            valueSetStorage.addItem(dimensionAttributeValue);
        }            
    }    
 
    result = valueSetStorage.save();
    return result;
}

Thursday, January 4, 2018

D365 - Security Fix Crashes SQL Server 2016

Happy New Year.

Yesterday, I get a call from the project team.  Apparently, they found a bug that needs an emergency fix.  This was a project that I haven't looked at in over a month.  Starting my VM, I found out that my SQL Server Instance was crashing.  The message that I kept getting was:

FCB::Open failed: C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\Binn\mssqlsystemresource.mdf
   1
   2(The system cannot find the file specified.)

My initial check indicated that that was a security issue that my logon user for my SQL Instance needed to granted access to the relevant directories.  However, my user was Network Service, so it should have full permissions for everything on the VM.

Further checking indicated that a security patch from MS was to blame.  Apparently, the files (both mdf and ldf) were moved to different location: C:\Program Files\Microsoft SQL Server\130\LocalDB\Binn.  Copying and pasting the files to the correct location solved the problem.

http://strictlysql.blogspot.ca/2016/11/sql-server-2016-sql-service-doesnt.html

Tuesday, December 12, 2017

IIS Compatibility Issues

Innocently working along in a small ASP.Net project (first time in a couple of years), a client noticed that a text area was displaying too thinly (about 50 pixels wide) when it was supposed to cover the full width of a tab page.  After checking around, I found that it was probably to compatibility mode in her IE11 settings.  Setting a meta tag to for IE9 compatibility solved the issue.

https://serverfault.com/questions/142721/iis-displaying-page-differently-when-localhost-is-used-in-url-vs-hostname
https://msdn.microsoft.com/en-us/library/cc288325(VS.85).aspx
https://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx

Monday, November 27, 2017

Allowing IIS Application Pools to Access Local Folders

I was working recently with a client's ASP.Net application and had a requirement to log various items to a file log.  One of the issues was how to grant the correct user read/write permissions to folder in question.  This article explains how to grant folder permissions to the user running a given application pool.

https://docs.microsoft.com/en-us/iis/manage/configuring-security/application-pool-identities

Essentially, it involves granting "IIS AppPool\DefaultAppPool" or other application pool name (as defined in IIS) the appropriate permission.

Friday, November 17, 2017

D365 - Windows Server 2016 Datacenter Re-Activation

The Windows edition on my VM for client development expired.  It then started shutting down every 1/2 hour.  Needless to say this is quite annoying.

After some googling, I found that the evaluation edition of Windows can be re-armed.  The command to do this in the command prompt is:

slmgr.vbs -dli

If that doesn't work, try:
slmgr.vbs -rearm

https://organicax.com/2014/10/03/windows-rearm
https://support.microsoft.com/en-us/help/948472/how-to-extend-the-windows-server-2008-evaluation-period

The articles say that the extension is 60 days,  however my VM says 180.

Thursday, October 26, 2017

Decrypt Web.Config for D365

If you need to get into a D365 database that is located on a D365 VM.  The credentials are located under <install drive>:\aosservice\\webroot.  However, they are encrypted.  To decrypt them, you just need to run the following in a console window:

<install drive>::\AOSService\webroot\bin\Microsoft.Dynamics.AX.Framework.ConfigEncryptor.exe -decrypt <install drive>::\AOSService\webroot\web.config

Just make sure that you open the console as an administrator.  Normal users do not have access to the RSA keys.

Monday, October 2, 2017

Note to self...

Do not define string EDTs with length of 4000 characters.  Ax uses Unicode, so 2000 characters is 4000 bytes.  As a result, inserts will generate an Invalid Precision Value error in SQL Server.  Use a memo type instead.

https://community.dynamics.com/ax/f/33/t/177918