Monday, June 25, 2018

D365FO - Importing License File (ISV/VAR Add-on) via deployable package into QA/Prod

Because we do not have the same access in QA/Production/Build as we do in the lower level environments installing a license file is a bit different compared to development, test, stage (any environment within the supplemental scope)

In order to deploy this license file to production or QA we need to do the following. It is good to note that creating this package is not environment specific. Meaning you can create this package on dev and deploy to prod or create on test and push it is QA or stage. It is just a set of pre-defined scripts which is why it doesn’t matter.

Any AOS server:

Windows Explorer > \AOSService\PackagesLocalDirectory\Bin\CustomDeployablePackage
*usually C:\AOSService\PackagesLocalDirectory\Bin\CustomDeployablePackage or on the K-drive*



In here you will find the file ImportISVLicense.zip

Make a copy of this zip file and open it up. Then browse to the following location: ImportISVLicense.zip\AosService\Scripts\License

Copy the license .txt file into this folder and save the zip.


NOTE: During my first try of this I extracted the zip, copied the file into it and zipped it back up. However during the next steps below that allow the license file to be applied I received an error message saying it has an invalid HotfixInstallationInfo.xml even though I did not touch this file. If you modify the zip directly LCS has no issues with the file so I would recommend not extracting the zip file but rather just using the default one as a template and copy the zip somewhere else and make the change there.

It is also good to note that you can put multiple license files in single package. But it is good note that it will import from alphabetically


LCS > Open the AX project in which you wish to apply the license to which in this case is the QA environment which means we need to open the implementation project. Then go to Asset library


Go to software deployable package > Click on the add button and fill out the info regarding the file being uploaded and select our zip file we created in the steps above

You will need to give it a couple of minutes before the "valid" box has a check mark. You will not be able to apply this file until the instance is marked as valid.

Once the instance has been marked as valid we need to apply it to the system.

Go to full details page for the specific environment we need to apply this to and click on the Maintain option > Apply updates




From this window we need to choose the asset we just created and click on apply



After this the system will process the request and trigger emails whenever the process starts and completes.

Whenever you open the main page you will also see the current status of the import itself as well.


From this point we can choose to either A. Do nothing and allow people to test or B. can mark the asset was as release candidate.

You should now be able to log back onto the environment  and see that the isv solution is now there

And that the license configuration page now has the license checked.


Friday, June 22, 2018

D365FO - OneBox Default account information

It seems like you have to dig through the internet to find onebox login information so I thought I would put it on here to make it easily find-able.

OneBox default windows login:

Login: local\Administrator
Password: pass@word1

OneBox SQL login
Login: axdbadmin   
Password: AOSWebSite@123


I will add more as I run across the need for different accounts that are associated with the OneBox.

D365FO - Maintenance mode / Importing License File (ISV/VAR Add-on License import)

In AX 2012 in order to import a license file for an ISV or VAR add-on all you simply did was go to your license management form and click on import, select the file and run a full sync. In D365FO this has changed dramatically.


In D365FO there is now only a license configuration which is available via  System administration > Setup > License configuration however you will notice that the data is read-only.

(This form is read-only unless the system is in the maintenance mode. Maintenance mode can be enabled in this environment by running maintenance job from LCS, or using Deployment.Setup tool locally)

In order to change this from read-only to modifiable we need to put the system in maintenance mode. We do this by opening the command prompt (cmd.exe)

change the dir to your local \AosService\PackagesLocalDirectory\Bin\ folder:

>cd C:\AosService\PackagesLocalDirectory\Bin\

Put the service into maintenance mode:

>Microsoft.Dynamics.AX.Deployment.Setup.exe --metadatadir C:\AosService\PackagesLocalDirectory --bindir C:\AosService\PackagesLocalDirectory\Bin --sqlserver . --sqldatabase axdb --sqluser [sql login] --sqlpwd [sqlpassword] --setupmode maintenancemode --isinmaintenancemode true

Reset IIS/Cycle the AOS> IISRESET


The system should now be in maintenance mode so we can import the file

via the cmd.exe prompt run the following

Import license file:
> Microsoft.Dynamics.AX.Deployment.Setup.exe --setupmode importlicensefile --metadatadir C:\AOSService\PackagesLocalDirectory --bindir C:\AOSService\PackagesLocalDirectory --sqlserver . --sqldatabase AxDB --sqluser [sql login] --sqlpwd [sql password] --licensefilename C:\licensefolder\license2018.txt


In D365FO you should now be able to open  System administration > Setup > License configuration and not see the read-only warning. You should see the name of the license that was import and you should be able to enable the configuration for the new license file.





Turn off maintenance mode

> Microsoft.Dynamics.AX.Deployment.Setup.exe --metadatadir C:\AosService\PackagesLocalDirectory --bindir C:\AosService\PackagesLocalDirectory\Bin --sqlserver . --sqldatabase axdb --sqluser [sql login] --sqlpwd [sqlpassword] --setupmode maintenancemode --isinmaintenancemode false



Reset IIS/Cycle the AOS: > iisreset


Note:
The example above was for importing it into a onebox. Depending on the environment you may need to change the drive from C:\* to K:\*
For this exe we list the metadatadir as the bin folder as well
The sqluser & sqlpassword will be different for each enviornment
The database name may be different depending on the environment


Reference:  D365FO - Importing License File (ISV/VAR Add-on) via deployable package into QA/Prod (importing license via deployable package)

D365FO - Execute a job/runnable class outside of visual studio

In AX 2012 you had access to the AOT to run a job in non-prod and in production. However because you can no longer access the server itself nor visual studio which is now the AOT you may need to run a custom job created outside of visual studio. In order to do this we can run the following url

[D365FO main URL]/?cmp=[company]&mi=SysClassRunner&cls=[job/runnable class name]

OneBox Example:
https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=[company]&mi=SysClassRunner&cls=[job/runnable class name]
Example: https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=MyCompany&mi=SysClassRunner&cls=MyCustomJob

D365FO - Table Browser via web browser

In previous versions of AX you had a table browser in the AOT which allowed you to view the data, modify or delete it while executing any table methods that may have been associated with it. In D365 you can no longer access the table browser the same way as you not longer have access to the AOT in production. 

In non-production environment's and production environment you can use the following URL to open a web based table browser

You can access it via environment url/?mi=SysTableBrowser&tableName=[tablename]&cmp=[company]

Example: https://environment.cloudax.dynamics.com/?mi=SysTableBrowser&tableName=Hcmworker&cmp=MHC

Where HcmWorker is the table and the company is MHC



In non-production environments which you have access to visual studio you can also access the web table browser by right clicking on a table and choosing "Open table browser"




It is good to note that when using the visual studio method to open the table browser the company that is on your user account will be defaulted to

Wednesday, December 27, 2017

Reset tts level - An unbalanced x++ ttsbegin/ttscommit pair has been detected

Sometimes a user may receive the message of "An unbalanced X++ ttsbegin/ttscommit pair has been been detected"

The following job will clear the issue and reset the tts level. I did not create this job originally and it is wildly used if you do a simple search on the internet. I am just posting this on here because I find myself looking for the script every now again 

static void resetTTS(Args _args)
{
    if (appl.ttsLevel() > 0)
    {
        info(strFmt("Level %1 aborted", appl.ttsLevel()));
        ttsAbort;
    }
}

Get all users missing a worker relationship assignment or retail channel

I found that sometimes when setting up a large amount of users in AX for retail it can lead to sometimes missing a worker relation configuration or missing a retail channel assignment. When this happens it wont break anything but some odd ball things can start to happen. I use the following job to make sure all of the users have at least a worker assignment and are assigned to a retail channel (when valid)

static void CheckUserSetup(Args _args)
{
    UserInfo userNames;
    DirPersonUser userRelations;
    MCRChannelUser retailChannelUsers;


    //find any users that are missing either a user relation or retail channel setup
    while select * from userNames
        outer join userRelations
        where userRelations.User == userNames.id
        outer join retailChannelUsers
        where retailChannelUsers.User == userNames.id
    {
        //check to see if there is a user relation
        if(!userRelations)
        {
            info(strFmt("No User Relation: %1 (%2)", userNames.id, userNames.name));
        }

        //check to see if the user is in a retail channel
        if(!retailChannelUsers)
        {
            info(strFmt("No Retail Channel: %1 (%2)", userNames.id, userNames.name));
        }
    }
}

Database index analysis

I find myself fine tuning the ax database more frequently to get the best possible performance. I have noticed that sometimes the default dba scripts to rebuild, reorganize indexes and update statistics for tables don't always run properly.

I use the following to break down the current avg fragmentation for a specific table

SELECT a.index_id, name, avg_fragmentation_in_percent 
FROM sys.dm_db_index_physical_stats (DB_ID(N',<database name>'),
      OBJECT_ID(N'dbo.<table name>'), NULL, NULL, NULL) AS a 
    JOIN sys.indexes AS b
      ON a.object_id = b.object_id AND a.index_id = b.index_id; 

I also use the following script to look at overall indexes which  may need attention. The below will generate the commands to rebuild or reorganize a fragmented index along with updating a specific tables index instead of relaying on the dba scripts.

DECLARE @fragmentPercent int = 30;
SELECT OBJECT_NAME(ind.OBJECT_ID) AS Tablename,
ind.name AS IndexName, indexstats.index_type_desc AS IndexType,
indexstats.avg_fragmentation_in_percent,
'ALTER INDEX ' + ind.name + ' ON ' + OBJECT_NAME(ind.OBJECT_ID) + ' REBUILD;' as RebuildIndex,
'ALTER INDEX ' + ind.name + ' ON ' + OBJECT_NAME(ind.OBJECT_ID) + ' REORGANIZE;' as ReorganizeIndex,
'UPDATE STATISTICS ' + OBJECT_NAME(ind.OBJECT_ID) + ' WITH FULLSCAN;' as UpdateStatics 
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, NULL) indexstats
INNER JOIN sys.indexes ind 
ON ind.object_id = indexstats.object_id
AND ind.index_id = indexstats.index_id
WHERE indexstats.avg_fragmentation_in_percent >= @fragmentPercent  and indexstats.index_type_desc <> 'HEAP'
ORDER BY indexstats.avg_fragmentation_in_percent DESC

Friday, November 18, 2016

Format phone number for us domestic and international

Currently in AX the telephone number fields allow you to enter anything into the phone number field. The following method shows how to take whatever the user entered and format it according to usa domestic and international formats. It also supports if the user entered an extension into the field instead of the extension field.  But if they did it would only work if they started the extension with the letter 'x'

The method assumes that you are currently located within the usa.

If you are reading this and are located outside of the us I would appreciate any feedback on the formatting of the international instances.

/// <summary>
/// Format phone number into a friendly display
/// </summary>
/// <param name="_phone">
/// Phone number to format
/// </param>
/// <returns>
/// Formatted display of phone number
/// </returns>
static public LogisticsElectronicAddressLocator FormatPhoneNumber(Phone _phone)
{
    LogisticsElectronicAddressLocator unformattedPhoneNumber, newPhoneNumber, formattedPhoneNumber;
    ListIterator iterator;
    str char;
    int phoneLength;
    Counter currentCharPos;
    Phone extension;
    Counter phonePartCounter;
    List phoneParts = new List(Types::String);
    container numbers = ['0','1','2','3','4','5','6','7','8','9']; //valid numbers of a phone number

    //split the phone number and extension (if available)
    phoneParts = strSplit(strUpr(_phone), "X");

    iterator = new ListIterator(phoneParts);

    //put the phone number and extension into 2 variables so we can apply formatting separately
    while (iterator.more())
    {
        if(phonePartCounter == 0)
        {
            //first part will contain the actual phone number
            unformattedPhoneNumber = iterator.value();
        }
        else if(phonePartCounter == 1)
        {
            //second part will contain the extension
            extension = iterator.value();
        }
        ++phonePartCounter;
        iterator.next();
    }

    //get the length of the phone number with the extension removed
    phoneLength = strlen(unformattedPhoneNumber);

    //remove all non numbers from field text
    for (currentCharPos=1; currentCharPos<=phoneLength; currentCharPos++)
    {
        char = substr(unformattedPhoneNumber,currentCharPos,1);
        if (confind(numbers,char))
        {
            newPhoneNumber = newPhoneNumber + char;
        }
    }

    //get the new length after any special chars have been removed so we know what type of phone number we are dealing with
    phoneLength = strlen(newPhoneNumber);

    //format the phone number according to length
    if(phoneLength == 10)
    {
        //local number
        formattedPhoneNumber = strFmt("(%1) %2-%3",substr(newPhoneNumber,1,3), substr(newPhoneNumber,4,3), substr(newPhoneNumber,7,4));
    }
    else if(phoneLength == 11)
    {
        //local with the +1
        formattedPhoneNumber = strFmt("+%1 (%2) %3-%4",substr(newPhoneNumber,1,1), substr(newPhoneNumber,2,3), substr(newPhoneNumber,5,3), substr(newPhoneNumber,8,4));
    }
    else if(phoneLength == 12)
    {
        //international with a country code length of 2
        formattedPhoneNumber = strFmt("+%1 %2 %3",substr(newPhoneNumber,1,2), substr(newPhoneNumber,3,4), substr(newPhoneNumber,7,6));
    }
    else if(phoneLength == 13)
    {
        //international with a country code length of 3
        formattedPhoneNumber = strFmt("+%1 %2 %3",substr(newPhoneNumber,1,3), substr(newPhoneNumber,4,4), substr(newPhoneNumber,8,6));
    }
    else if(phoneLength == 14)
    {
        //international with a country code dialed from another country
        formattedPhoneNumber = strFmt("%1-%2-%3-%4-%5",substr(newPhoneNumber,1,2), substr(newPhoneNumber,3,2), substr(newPhoneNumber,5,2), substr(newPhoneNumber,7,3), substr(newPhoneNumber,10,5));
    }
    else
    {
        //no format found so just use the phone number the user inputed
        formattedPhoneNumber = newPhoneNumber;
    }

    //add the extension back onto the newly formatted phone number
    if(extension != "")
    {
        formattedPhoneNumber = strFmt("%1 x%2", formattedPhoneNumber, extension);
    }

    return formattedPhoneNumber;
}



Some of the results are as follows:
3121234567 = (312) 123-4567
(312) 123-4567 = (312) 123-4567
1(312) 123-4567 = +1 (312) 123-4567
13121234567 = +1 (312) 123-4567
131212345678 = +13 1212 345678
131212345679 = +13 1212 345679
1312123456789 = +131 2123 456789
13121234567899 = 13-12-12-345-67899
13121234567 x2345 = +1 (312) 123-4567 x2345

Calling method dynamically through DictClass

Sometimes we need to loop through all of the parameters that have been defined within a class or you need to call a method dynamically without knowing what the method name is until you get it from another source.

The following shows how to get a method name from a table and call said method (in our case a parameter method name of type NoYesId) and get the result

This example shown below is being called within a class. If you called it outside of the class you would replace the 'this' object with the class instance name.

Example: TestClass currentTest = new TestClass() you would replace 'this' with currentTest.

validationMap.ParmName = name of method

 

    NoYesId testPass;
    DictClass dictClass;
    ExecutePermission perm;
    TableThatHasParmMethodListing validationMap;


    //allow the code to run
    perm = new ExecutePermission();
    perm.assert();

    //go through all of the require test mappings that will link up to the current class being executed
    while select * from validationMap
    {
        //class the validation parm mapping for the current record
        dictClass = new DictClass(classidget(this));
        testPass = dictClass.callObject(validationMap.ParmName, this);

//testPass variable will now have the result of the parm
    }

    //revert code access
    CodeAccessPermission::revertAssert();