Showing posts with label D365FO. Show all posts
Showing posts with label D365FO. Show all posts

Thursday, August 29, 2024

D365FO - SSRS - The Configuration/Dsn application cannot be run in Win32 mode.

 

Recently a D365FO Tier-1 CHE SSRS instance completely stopped working. What was strange is the service could be stopped and started with no issues. But if you look at event viewer 5mins after it was started you would start to see the following error message.

 

Report Server(WMI10) : The Configuration/Dsn application cannot be run in Win32 mode.

 

In order to resolve the issue I tried the following

 1.) restoring C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer\rsreportserver.config from another server

2.) updating SSRS

3.) Repair installation of SSRS 

 

However nothing seemed to work until I did the following to completely remove and start with a fresh instance of SSRS and then remap it to D365

 

1.)    completely uninstall  SSRS

2.)    install ssrs

3.)    remove the old ssrs database

4.)    create a new database using the same names within SSRS config

5.)    run J:\AosService\PackagesLocalDirectory\Plugins\AxReportVmRoleStartupTask\DeploySsrsExtension.ps1 but you need to make sure you let it finish. It gives you the impression its done but  really isn’t until 10-15mins later  (D365 will not recognize this as a valid SSRS instance until this is done even though its running)

6.)     after the above completes it is letting me run the .deploy report script for reports and apply platform updates again

Monday, July 29, 2024

D365FO - Tier-1 - CHE - SSRS Wont Start - RSHostingService.exe Description: The process was terminated due to an unhandled exception. Exception Info: System.Xml.XmlException (Cloudstrike)

It would appear the CrowdStrike issue that plagued PC's and server's worldwide may cause some SSRS instances to stop being able to be started on a CHE Tier-1 for D365FO.

The following error below may be present within event viewer when trying to start the SSRS service. This issue is due to the corruption of a SSRS web.config file

Application: RSHostingService.exe 
Framework Version: v4.0.30319 
Description: The process was terminated due to an unhandled exception. 
Exception Info: System.Xml.XmlException at System.Xml.XmlTextReaderImpl.Throw(System.Exception) at System.Xml.XmlTextReaderImpl.ThrowInvalidChar(Char[], Int32, Int32) at System.Xml.XmlTextReaderImpl.ParseRootLevelWhitespace() at System.Xml.XmlTextReaderImpl.ParseDocumentContent() at System.Xml.XmlReader.MoveToContent() at System.Xml.Linq.XElement.Load(System.Xml.XmlReader, System.Xml.Linq.LoadOptions) at System.Xml.Linq.XElement.Load(System.String, System.Xml.Linq.LoadOptions) at Microsoft.BIServer.Configuration.ConfigReader..ctor(System.String) at Microsoft.BIServer.BIService.ServiceConfig..ctor(System.String) at Microsoft.BIServer.BIService.BIService.SetNewConfigFilePathIfExists(System.String) at Microsoft.BIServer.BIService.BIService..ctor() at Microsoft.BIServer.BIService.Program.Main() 

Faulting application name: RSHostingService.exe, version: 15.0.1102.1084, time stamp: 0x64b6b433 
Faulting module name: KERNELBASE.dll, version: 10.0.20348.2400, time stamp: 0x784b07c7 Exception code: 0xe0434352 
Fault offset: 0x000000000003f19c 
Faulting process id: 0x2824 
Faulting application start time: 0x01dae1f335c6e225 
Faulting application path: C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\RSHostingService\RSHostingService.exe 
Faulting module path: C:\Windows\System32\KERNELBASE.dll 


In order to fix the issue the following needs to occur

1.) Go to C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer
2.) Rename web.config to web.config.old or web.config.bak
3.) Copy new web.config to C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer
4.) Open Services.msc
5.) Start  SQL Server Reporting Services


You can download the needed default web.config here


You may also run into the following files needing to be replaced as well

C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\ReportServer\rssrvpolicy.config

C:\Program Files\Microsoft SQL Server Reporting Services\SSRS\Portal\RSPortal.exe.config

You can tell if it needs to be replaced or not by simply opening the text file in note pad. If its blank its needs replaced if you can see text you are fine.


Thursday, May 23, 2024

D365FO - Step 57: GlobalUpdate script for service model: RetailServer on machine / Step 59: Update script for service model: RetailCloudPos on machine: Failing

 

I recently ran into an interesting issue when it comes to applying a platform update to a CHE environment for D365. I have seen retail errors before but is it usually based on the retail server and step 57

 

Example:

Step 57: GlobalUpdate script for service model: RetailServer on machine: [CHE name]

 

Which can be fixed by simply going to the service volume (usually K) / Deployable packages/ Runbook Id / RetailServer / Scripts / DropAllRetailChannelDbObjects.sql and running it against the AxDB and then hitting resume.

IE. K:\DeployablePackages\{guide}\RetailServer\Scripts\DropAllRetailChannelDbObjects.sql

 

How this time around I am getting an odd error around step 59 which isnt documented anywhere.

 

Step 59: Update script for service model: RetailCloudPos on machine: CAM-DEV-CMMH-1

 

The step 59 failed with the following error:

The step execution time exceed the timeout value specified: 10min

 

Which is quite odd, because this was a brand new box with no data or custom code loaded.

 

In order to resolve this issue what need to do is go into the same deployable packages as the dropallretail objects fix but this time we will go to

service volume (usually K) / Deployable packages/ Runbook Id / RetailCloudPos\Scripts\Upgrade\Core\ UpdateCloudPos.ps1  and clear all data from the file UpdateCloudPos.ps1 so it is empty but the file is still ran. Restart the server and then hit resume inside of LCS.

IE: K:\DeployablePackages\{guid}\RetailCloudPos\Scripts\Upgrade\Core\UpdateCloudPos.ps1  

 

 

When this occurs and just hitting resume still fails on a generic message it may be worth while to check out

service volume (usually K) / Deployable packages/ Runbook Id / DefaultServiceModelData.xml

 

IE: K:\DeployablePackages\{guid}\ DefaultServiceModelData.xml

 

Which will give you a list of all the powershell scripts involved with the update process.

 

Thursday, February 29, 2024

D365FO - Generating ZPL Label code based on template replacement

 While exploring how the advanced warehouse document routing feature worked in order to generate zpl code based on a dataset which can be dynamically defined I kept running into road blocks on using the default D365 code base because of the internal flag being set on this class base. The code that is out of the box really wants to to only be able to print labels and never view them which is kind of silly if you think about it.  Of course there is also nothing documented about these classes  (yet) on any of the boards, blogs, official channels.

Because of this I thought I would document how the replacement feature within these classes works will allows you to dynamiclly generate ZPL based on a user defined query within the UI. This is quite interesting and could be used elsewhere within the platform to allow you define a custom query and then have a replacement based on it. I know there are many products out there that already do this but figure I would show how to do this on your own as the majority of people still follow the approach of defining the tables and variables that need replaced within the code base (guilty as charged)


Key tables to understand:

WHSLabelLayout: this will house the header record and generic settings for the zpl template

WHSLabelLayoutVersion: this will house the ZPL code and contains the template in which we will need

WHSLabelLayoutDataSource: this will house the dynamics data source query object which can be defined dynamicly from the user but based on the main layouts calling record record.


Key classes to understand:

WhsCustomLabelPrintCommandGenerator: WHS class that will extend the base class WhsLabelPrintCommandGenerator. This class is used to overload some of the methods in order to define a custom query object based on what was defined within WHSLabelLayoutDataSource

WhsLabelPrintCommandGenerator: WHS base class which contains the overall calling logic to generate the labels as well as print them based on being dynamic or out of the box structure.

WhsDocumentRoutingTranslator: WHS translator object which will hold the structure of replacing things like $Table.FieldName$, queryRun or language objects that would be related to the variables. 

 WhsDocumentRoutingTemplateTranslator: WHS class that will allow you to take the translator, queryRun, zpl template and  execute a find and replaced based on dynamic variables defined within the template based on the query structure defined by the user



Whats really great about this approach from MS is that it will basically allow you to apply this logic to any scenario where you want to dynamically define variables based on field listing or methods on tables vs hardcoding a strReplace.


The following will show you how to take a template for a purchase order (PurchTable) which has a custom query and apply it to a zpl template. However it is good to note there is another type of template (out of the box) which I did not tackle.



 

        PurchTable purchTable = PurchTable::find(_purchId);
        //find the specific layout we want to use based on the name
        WHSLabelLayout labelLayout = WHSLabelLayout::find(WHSLabelLayoutType::CustomLabel, "Purchase orders");
        WHSLabelLayoutVersion layoutSourceVersion;

        //find the specific layout version which houses the zpl code
        select firstonly * from layoutSourceVersion where layoutSourceVersion.LabelLayoutId == labelLayout.LabelLayoutId;

       
        if(purchTable && labelLayout && layoutSourceVersion)
        {
            //create the initial translator object
            WhsDocumentRoutingTranslator translator = WhsDocumentRoutingTranslator::construct();

            if (labelLayout.LabelLocale)
            {
                //define the specific language
                translator.withLanguage(labelLayout.LabelLocale);
            }

            //is the layout a based on a custom template
            if (labelLayout.EnableTemplateTranslator)
            {
                QueryRun queryRun;
                Query newQuery;
                WHSLabelLayoutDataSource layoutDataSource;
                
                //grab the dynamic query that is defined for the specific label layout
                select firstonly DataSourceQuery from layoutDataSource
                    where layoutDataSource.LabelLayoutDataSourceId == layoutSourceVersion.LabelLayoutId;

                if (layoutDataSource && layoutDataSource.DataSourceQuery != conNull())
                {
                    newQuery = new Query(layoutDataSource.DataSourceQuery);
                    const FieldId RecIdFieldId = 65534; // Platform defined FieldId for RecId field (see Query/Constants.cs in Platform for example)
                    //add in a filter to the new query object so we pull down the specific record
                    newQuery.dataSourceNo(1)
                    .addRange(RecIdFieldId)
                    .value(queryValue(purchTable.RecId));
                }

                //convert the query object so we can pass it to the translator
                queryRun = new QueryRun(newQuery);
                translator.withRecordsFromQueryRun(queryRun);
                
                //define the original layout zpl code that needs to populated with dynamic variables
                WHSZPL layoutSource = layoutSourceVersion.zpl;
                
                //define a new translator object for the document routing structure based on the translator object which has our query which needs to be applied to the zpl code source
                WhsDocumentRoutingTemplateTranslator newZPLLabels = WhsDocumentRoutingTemplateTranslator::newFromTemplateAndQueryRun(layoutSource, queryRun)
                    .withTranslator(translator);
                
                //apply the queryRun object to the template via the translator
		List labelList = newZPLLabels.translateTemplate();
		//convert to string
                str zplStringWithData = labelList.toString();

		//TODO: now that we have raw dynamic zpl code we can do whatever we want with it. IE Send it to printer, create an image as a print preview, create a pdf, create an event that can manually do things without the build in prompt
                

                

            }
            else
            {
                //TODO:: what about when a default label is called? need to look into WhsLabelPrintCommandGenerator.printLabels() second half where the it calls the provider to generate the query run
                //WHSZPL outputLabel = translator.translate(layoutSource);
                //List labelsList = this.translateLabelTemplate();
            }



Tuesday, February 27, 2024

D365FO - ZPL Printer Emulation (labelary.com) directly within D365FO

It would appear that recently the Chrome ZPL printer emulator is no longer valid which is triggering some people to install a ZPL printer emulator in order to validate Dynamic ZPL code within D365FO. After much research it appears that all these extensions and printer emulators all use the free Label service https://labelary.com/

Because this API is being used throughout the industry why not create our own way to call this API directly within D365 vs installing a printer emulator which then requires a document routing agent to be installed.

The below code snippet shows you how to connect to their RESTful API service which can generate various types of images based on the ZPL codes. You could easily modularize the following logic in order to create something pretty powerful and dynamic based on any dataset.


Webservice to generate a label based on ZPL code: https://labelary.com/service.html

Online ZPL Viewer: https://labelary.com/viewer.html 


 

            System.Exception clrError;
            InteropPermission interopPermission;
            System.Net.HttpWebRequest request;
            System.IO.Stream stream;
            System.Byte[] zpl = System.Text.Encoding::UTF8.GetBytes("^xa^cfa,50^fo100,100^fdHello World^fs^xz"); //raw zpl code

            request = System.Net.WebRequest::Create("http://api.labelary.com/v1/printers/8dpmm/labels/4x6/0/") as System.Net.HttpWebRequest;
            request.Method = 'POST';
            request.Accept = "image/png";
            request.ContentType = 'application/x-www-form-urlencoded';
  
            try
            {
                interopPermission = new InteropPermission(InteropKind::ComInterop);
                interopPermission.assert();

                // send out the payload
                using (System.IO.Stream dataStream = request.GetRequestStream())
                {
                    dataStream.Write(zpl, 0, zpl.Length);
                }

                using (System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse)
                {
                    stream = response.GetResponseStream();
                    System.IO.MemoryStream filestream = new System.IO.MemoryStream();
                    
                    //we need to convert the main stream to a memory stream in order to access the binary objects
                    stream.CopyTo(filestream);
     
                    //send the memory stream directly to the user which now contains a file object
                    File::SendFileToUser(filestream, "test.png");
                    
                    //convert the memory stream into an image object which can then be displayed to the user
		    BinData binData = new BinData();
                    container baseContainer;
                    Image image = new Image();
                    
                    baseContainer =  Binary::constructFromMemoryStream(fileStream).getContainer();

                    binData.setData(baseContainer);
                    image.setData(binData.getData());
                    //display the newly created image to the user
		    FormImageControl1.image(image);
                }
                Info("done");
            }
            catch (Exception::CLRError)
            {
                //catch any clr error
                clrError = CLRInterop::getLastException();
                if (clrError != null)
                {
                    clrError = clrError.InnerException;
                    throw error(clrError.ToString());
                }
            }
            finally
            {
                CodeAccessPermission::revertAssert();
            }	 





D365FO - Converting a System.IO.MemoryStream object to Image

 Sometimes we need to either generate or download an image from a 3rd party service and want to serve up the image within the UI.


Below is an example on how to convert a MemoryStream Object that was generated from an HttpWebResponse into something that is viewable on screen or can be saved into the data base



 
	 
                using (System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse)
                {
                    stream = response.GetResponseStream();
                    System.IO.MemoryStream filestream = new System.IO.MemoryStream();
                    
                    //we need to convert the main stream to a memory stream in order to access the binary objects
                    stream.CopyTo(filestream);
     
                   
                    BinData binData = new BinData();
                    container baseContainer;
                    Image image = new Image();
                    
                    //convert the memory stream into a container object which can then be converted into a binData object which can then be defined on an image object which can then be displayed to the end user
		    baseContainer =  Binary::constructFromMemoryStream(fileStream).getContainer();

                    binData.setData(baseContainer);
                    image.setData(binData.getData());
                    //define the image control on a form to the image that was generated from the memory stream
		    FormImageControl1.image(image);
                }   

D365FO - Calling external RESTful API via POST to download a file and send to the user

 

I have recently found a mixture of AX 2012 / D365 code online when it comes to calling external REST web API’s directly from AX/D365FO. It seems as though in today’s world the majority of people only care about calling and receiving JSON or XML or some type of text-based object which the majority of time holds true. But what happens when that API should return some type of file or object rather than raw text where we want to save or serve up the file?

This is where it gets tricky because the System.IO.StreamReader like shown within the majority of examples online doesn’t provide the structure that we need in order to access binary objects to be able to send a file to the user or display something on the screen. It is however great when it comes to reading text based responses.

The key to understanding  the framework is once we are able to put the main System.IO.Stream into a System.IO.MemoryStream then we are easily able to convert the binary objects to various output's.

 

The best way to describe is that System.IO.Stream operates much like a FormRun object. It is the top most level and once we get access to that then we can access any child objects like StreamReader or MemoryStream which can then be handled differently based on the scenario.


Below is an example how to call a generic RESTful API via a post (the request structure may change based on your specific API being called)



 
	    System.Exception clrError;
            InteropPermission interopPermission;
            System.Net.HttpWebRequest request;
            System.IO.Stream stream;
            System.Byte[] zpl = System.Text.Encoding::UTF8.GetBytes("post body");

            request = System.Net.WebRequest::Create("API URL") as System.Net.HttpWebRequest;
            request.Method = 'POST';
            request.Accept = "image/png"; //document type to generate
            request.ContentType = 'application/x-www-form-urlencoded';
  
            try
            {
                interopPermission = new InteropPermission(InteropKind::ComInterop);
                interopPermission.assert();

                // send out the payload
                using (System.IO.Stream dataStream = request.GetRequestStream())
                {
                    dataStream.Write(zpl, 0, zpl.Length);
                }

                
                using (System.Net.HttpWebResponse response = request.GetResponse() as System.Net.HttpWebResponse)
                {
                    stream = response.GetResponseStream();
                    System.IO.MemoryStream filestream = new System.IO.MemoryStream();
                    
                    //we need to convert the main stream to a memory stream in order to access the binary objects
                    stream.CopyTo(filestream);
     
                    //send the memory stream directly to the user which now contains a file object
                    File::SendFileToUser(filestream, "test.png");
                }
            }
            catch (Exception::CLRError)
            {
                //catch any clr error
                clrError = CLRInterop::getLastException();
                if (clrError != null)
                {
                    clrError = clrError.InnerException;
                    throw error(clrError.ToString());
                }
            }
            finally
            {
                CodeAccessPermission::revertAssert();
            }




Tuesday, November 26, 2019

D365FO / C# - Accessing the InfoLog / InfoLogEntries Object within a custom webservice

While working on testing a custom web-service that originally tried to read the info log and report it back to a field in web service via custom logic I noticed that that error message being returned was actually pretty generic and told me nothing about the issue.

After debugging the code it looks like the info log itself is now being returned automatically with a SOAP / JSON web service which is great because this actually tells us why a record cant be created or what the issue is in detail with no extra code. However it appears nothing is logged online about this and it doesn't appear to work like a contract class response and which would allow you to use a foreach() on the object. Instead we need to treat it like an array based object as the code below shows

Whats cool about this is that it actually returns the type of message from the infolog as well (Info, Warning, Error) so you can add in logic that will tell you what exactly the issue is.

 //call out the infolog class  
 [webserviceName].Infolog messages = new [webservicename].Infolog();  
 //read in the infolog from the response  
 messages = response.Infolog;  
 //get the length of the log and read in each message  
 for(int messagePosition = 0; messagePosition <= messages.Entries.Length - 1; ++messagePosition)  
 {  
    AzureLog.AppendText(String.Format("Info Log Message {1}: {0}", messages.Entries[messagePosition].Message.ToString(), messages.Entries[messagePosition].Type.ToString()) + Environment.NewLine);  
 }  




The output will then look something like this


Info Log Message Info: A custom info statement based on an extension of salesTable.ValidateWrite()
Info Log Message Warning: Site [SITENAME] does not exist.
Info Log Message Warning: Warehouse [WAREHOUSENAME] is not connected to site [SITENAME]
Info Log Message Warning: Warehouse [WAREHOUSE] does not exist.
Info Log Message Warning: Site [SITE] does not exist.
Info Log Message Error: Update has been canceled.







Wednesday, August 21, 2019

D365FO - BYODB Entity Export Failing with no error message (Catch Weight Exporting Issue)


Recently after standing up a new export and going to export the Sales order lines V2 and Purchase order lines V2 entity's to a Azure SQL DB it started to fail. However in the execution log there is no error





If you try running it in batch mode the batch completes but nothing is export but the export shows as completed.


If you dig into event viewer on the server under

Application and Services Logs > Microsoft > Dynamics > AX-DIXFRuntime > Operational

You will find your error but it is very generic and does not give you any helpful information just that entity is crashing







Error:
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.Exception: Exception from HRESULT: 0xC0202009 at Microsoft.Dynamics.AX.Framework.Tools.DMF.ServiceProxy.DmfEntitySharedTypesProxy.DoWork[T](Func`1 work) --- End of inner exception stack trace --- at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor) at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.RuntimeType.InvokeMember(String name, BindingFlags bindingFlags, Binder binder, Object target, Object[] providedArgs, ParameterModifier[] modifiers, CultureInfo culture, String[] namedParams) at Microsoft.Dynamics.Ax.Xpp.CLRInterop.MakeReflectionCall(Object instance, String methodName, Object[] parameters)



After trying multiple ways of regenerating the mapping, refreshing the entity list, builds, db syncs I was still getting the error.

However I deleted all fields within the mapping and just did a few base fields the entity index is based on and found that the entity would export. Because of this I knew that something was wrong with one of the field mappings.


After regenerating the complete list and started to remove fields in order to see if it would work I was able to find the specific fields that were cause the issue.

ORDEREDCATCHWEIGHTQUANTITY


Currently we have Catch Weight enabled within the configuration key but are not using this feature as we are still in the design phase of our implementation


If we have an entity that is failing export and it is a base export one thing to do is whenever defining the mapping or the scheme do a search for "catch" and remove it from the mapping (in data project assignment or main modify target mapping) or  the export will now work. It does not matter if the fields get published to the scheme it only matters if these catch weight fields get included in the export.



Update 9/26/19: It turns out that disabling the catchweight key does not stop the field from being added to the entity mapping.

Monday, July 15, 2019

D365FO / Setting default values for custom fields on a vendor invoice from a purchase order (VendInvoiceInfoTable / PurchFormletterParmDataInvoice)

When creating a vendor invoice and you are trying to populate custom fields on table VendInvoiceInfoTable you may find that the following will only work if you are creating a blank invoice and not executing it from the invoice create button on a purchase order



[DataEventHandler(tableStr(VendInvoiceInfoTable), DataEventType::InitializingRecord)] 
 public static void VendInvoiceInfoTable_onInitializingRecord(Common sender, DataEventArgs e) 
 { 
      VendInvoiceInfoTable vendInvoiceInfoTable = sender as VendInvoiceInfoTable; 
      vendInvoiceInfoTable.CustomField   VendParameters::find().DefaultCustomField; 
 }

This is due to the class PurchFormletterParmDataInvoice.createInvoiceHeaderFromTempTable()
which called .skipEvents(true), skipDataMethods(true), .skipDatabaseLog(true) and calls a Query::insert_recordset() because of this none of the default events will get triggered.

In order to properly populate customfields on a blank vendor invoice or creating once from a purchase order the following will need to be done via extensions and CoC.


  1. Add custom fields to VendInvoiceInfoTable
  2. Add custom fields to VendInvoiceInfoTableTmp (name and types need to match what was created on VendInvoiceInfoTable)
    1. We have to add it to this table because on PurchFormLetterParmDataInvoice.insertParmTable() it calls a buf2buf command which loops through the field list.
  3. Create extension class of table VendInvoiceInfoTable and apply a CoC of defaultRow method which will populate the value onto both the regular and tmp instance of VendInvoiceInfoTable however it will not save to the database unless the remaining steps are completed
    1. /// <summary>
      /// Default values for a vendor invoice header from a purchase order
      /// </summary>
      [ExtensionOf(tableStr(VendInvoiceInfoTable))]
      final class APInvoiceModsVendInvoiceInfoTable_Extension
      {
          /// <summary>
          /// COC table method defaultRow which resets default valies
          /// </summary>
          /// <param name = "_purchTable">PurchTable</param>
          /// <param name = "_ledgerJournalTrans">LedgerJournalTrans</param>
          /// <param name = "_resetFieldState">Reset field state</param>
          public void defaultRow(PurchTable _purchTable, LedgerJournalTrans _ledgerJournalTrans, boolean _resetFieldState)
          {
              CustomEDTType defaultCustomField;
              
      next defaultRow(_purchTable,_ledgerJournalTrans,_resetFieldState);

              //find the default custom field from vend parms
      defaultCustomField = VendParameters::find().DefaultCustomField;
              
      //save the default invoice type to the header which will get copied to    VendInvoiceInfoTableTmp and then back to the main table due to PurchFormLetterParmDataInvoice.createInvoiceHeaderFromTempTable
      this.CustomField = defaultCustomField;

          }

      }
  4. Create extension class of PurchFormLletterParmDataInvoice and apply a CoC to buildCreateInvoiceHeaderFromTempTableFieldQuery and buildCreateInvoiceHeaderFromTempTableFieldMap because the method PurchFormletterParmDataInvoice.createInvoiceHeaderFromTempTable() creates a dynamic map of the fields to copy from the temp table into the main table 
    1. [ExtensionOf(classStr(PurchFormletterParmDataInvoice))]
      final class APInvoiceModsPurchFormletterParmDataInvoice_Extension
      {
          /// <summary>
          /// COC method that adds our custom field to the Field Query selection  from the temp table
          /// </summary>
          /// <param name = "_qbdsVendInvoiceInfoTableTmp">VendInvoiceInfoTableTmp</param>
          protected void buildCreateInvoiceHeaderFromTempTableFieldQuery(QueryBuildDataSource _qbdsVendInvoiceInfoTableTmp)
          {
              next buildCreateInvoiceHeaderFromTempTableFieldQuery(_qbdsVendInvoiceInfoTableTmp);

              //add custom selection field
      _qbdsVendInvoiceInfoTableTmp.addSelectionField(fieldNum(VendInvoiceInfoTableTmp, CustomField));
          }

          /// <summary>
          /// COC method thats adds our custom field to the dynamic field map against the main table. Which will get copied from the selection query
          /// </summary>
          /// <param name = "_qbdsVendInvoiceInfoTableTmp">VendInvoiceInfoTableTmp</param>
          /// <returns>Modified dynamic map to insert into the db</returns>
          protected Map buildCreateInvoiceHeaderFromTempTableFieldMap(QueryBuildDataSource _qbdsVendInvoiceInfoTableTmp)
          {
              var targetToSourceMap = next buildCreateInvoiceHeaderFromTempTableFieldMap(_qbdsVendInvoiceInfoTableTmp);
              targetToSourceMap.insert(fieldStr(VendInvoiceInfoTable, CustomField), [_qbdsVendInvoiceInfoTableTmp.uniqueId(), fieldStr(VendInvoiceInfoTableTmp, CustomField)]);
             
      return targetToSourceMap;
           }

      }

By applying our populate method to the defaultRow() method this will also trigger the value to populated on a blank vendor invoice once a vendor account is selected.

Wednesday, December 5, 2018

D365FO / Azure - Create website redirect in azure to handle supplemental environment url changes

Currently while dealing with D365FO supplemental environments within LCS that are all hosted on a single machine anytime we update them and there is an error microsoft's recommended fix is to stand up a new environment. Because of this the url will change and you need to notify your users. In order to handle this scenario so our users do not need to worry about a new url you can create a redirect website in azure that you can point to whatever url you want and if your users bookmark these urls instead of the direct ones then you don't need to worry about who has what book marked.

Azure portal 

Step 1. In the Azure portal you need to create a webapp. You do this by going to "Create a resource" and then choose a web app.




Step 2. In the newly created webapp go to Advanced tools (Kudu)


Step 3. In Kudu go to Debug console > cmd


Step 4. Browse to /site/wwwroot and click on the add ("+") button and choose to create a new file and name it web.config

Step 5. Click on the edit button for the web.config file and add in the following and hit save.


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <rule name="Redirect test enviornment" stopProcessing="true">
                    <match url="^test" />
                    <action type="Redirect" url="https://testurl.cloudax.dynamics.com" redirectType="Permanent" />
                </rule>   
                <rule name="Redirect stage enviornment" stopProcessing="true">
                    <match url="^stage" />
                    <action type="Redirect" url="https://stageurl.cloudax.dynamics.com" redirectType="Permanent" />
                </rule>
                <rule name="Redirect dm enviornment" stopProcessing="true">
                    <match url="^dm" />
                    <action type="Redirect" url="https://datamigrationurl.cloudax.dynamics.com" redirectType="Permanent" />
                </rule>
                <rule name="Redirect qa enviornment" stopProcessing="true">
                    <match url="^qa" />
                    <action type="Redirect" url="https://qaurl.operations.dynamics.com/" redirectType="Permanent" />
                </rule>
                <rule name="Redirect header" stopProcessing="true">
                    <match url=".*" />
                <action type="Redirect" url="https://www.google.com" redirectType="Permanent" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>    


Step 6. After hitting save you should now be able to access your azure web app in the following manner

https://yourwebappurl.com/test
https://yourwebappurl.com/qa
https://yourwebappurl.com/stage
https://yourwebappurl.com/dm

and they should now forward to their respective website.

If you do not include a /* tag and just put https://yourwebappurl.com then it should forward you to google.com as well





D365FO - Can not rename database - The database could not be exclusively locked to perform the operation

Currently in D365 when importing a new dataset via the bacpac method you are supposed to import the data into a new database and then rename the old AxDB to something else and then rename the new AxDB_new to AxDB. In order to do this previously we would turn off the following services
  • Microsoft Dynamics 365 Unified Operations: Batch Management Service
  • Microsoft Dynamics 365 Unified Operations: Data Import Export Framework Service
  • Management Reporter 2012 Process Service
  • World Wide Web Publishing Service (IIS/W3SVC)

Which SHOULD which should drop all connections to the AxDB. However I am noticing that now (in 8.1+) there are still connections from axOnline, axSystem and a .net provider that I am not sure where they are coming from. Which will cause the error

Unable to rename AxDB

The database could not be exclusively locked to perform the operation.

I have tried putting the db into single user mode and restricted mode however the connections still happen. So in order to rename the databases and drop the connections you can use the following script which will wrap up everything into a single transaction and seems to work well



--set main db to single connections and drop the existing connections
ALTER DATABASE AxDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE
--rename datbase to db_old
ALTER DATABASE AxDB MODIFY NAME = AxDB_old
--set the old db to multi user
ALTER DATABASE AxDB_old SET MULTI_USER
--rename the new db to the main db name
ALTER DATABASE AxDB_new MODIFY NAME = AxDB        

Monday, November 5, 2018

D365FO - Oracle Virtual Box Dupe UUID found on new OneBox version

Currently I am using Oracle VM VirtualBox Manager to run my onebox instance of D365FO for development. However anytime a new version comes out and I try to create a new instance of my onebox I get the following error

Cannot register the hard disk 'newD365FO.vhd' because a harddisk 'oldD365FO.vhd' with UUID already exists.

In order to fix the problem we need to do the following:

1. Open cmd.exe as admin and run the following
2. cd C:\Program Files\Oracle\VirtualBox\
3. VBOXMANAGE.EXE internalcommands sethduuid "C:\locationofd365vhd\FinandOps8.0withPlatUpdate15.vhd"



Once this has been completed continue adding the disk to a new vm setup like you normally would and you should no longer get the error.


Tuesday, August 28, 2018

D365FO - C#/X++ - Authentication & Custom web service examples

While starting my path with D365FO and the need to create custom web services to integrate outside systems with D365FO with no previous Azure experience I found myself spending a large amount of time trying to figure out how to properly authenticate with D365FO along with the different options we now have with D365FO compared to the old AX2012 AIF services.

There doesn't seem to be many resources online or in books about this and each one I was able to find seems to be structured differently compared if we are doing interactive logins, defined creds, or using the web api/secret key even though the difference is tiny.

Note: this is just my first stab at authentication so there might be better ways to do some of this. These are just examples that I have found to work well.


There are three different ways to accomplish authenticating with the D365FO system

1. Interactive login
2. Define username/password
3. Using secret key

Because option 2 requires the user the to login interactively before we can use it the examples I am providing will not go over it in my C# app but the code is listed at the bottom of this post.

If you receive the error The user or administrator has not consented to use the application with ID # named #. Send an interactive authorization request for this user and resource

It is because the exact scenario I decided has occurred and you need to login in interactively to grant it permission.

Files:
C# D365Auth - Download - This project includes the following:
The project includes the odataclient.cs/tt which is why it is so big

D365FO Azure Authentication - Interactive & Defined Creds (Secret key), Setting up a class to define multiple environments

SOAP - Calling custom webservice - single input/single output, No input/multiple output, Multiple inputs/multiple outputs, Single input/update record/single output(before and after update results)

JSON - No input/multiple outputs, Single input/single output

Odata - Calls customer entity to get basic customer info, filter customer entity by defined customer account

X++ D365FO - Download - This project includes the following:
Webservice class: method that expects inputclass outputs class, method that expects a string and returns a string, method with no parms and returns an output class, method that inspects 2 parms and returns a string.

It is good to note that I found if you name the service + service group with the same name you may run into issues even though it will compile.



Outside of downloading the project I thought I would post the 2 main methods  I have defined for interactive and secret key login so it is easy to tell the difference.

Interactive:
        public static AuthenticationResult GenerateAzureToken(AXEnvironments _environmentName)
        {
            //create the context of the token
            AuthenticationContext context = new AuthenticationContext(_environmentName.AzureADTenant, TokenCache.DefaultShared);
            
            //create token for the client 
            Task<AuthenticationResult> task = context.AcquireTokenAsync(_environmentName.AzureResource, _environmentName.NativeAzureClientAppId, new Uri(_environmentName.UriString), new PlatformParameters(PromptBehavior.Always));
            
            //wait for the task to finsh
            task.Wait();
            
            //return the result
            return task.Result;
        }

Defined:
        public static AuthenticationResult GenerateAzureToken(AXEnvironments _environment)
        {

            //create the credential object based upon the encrypted secret key in azure
            ClientCredential creds = new ClientCredential(_environment.WebAzureClientAppId, _environment.AzureClientSecret);

            //setup the context of the token
            AuthenticationContext context = new AuthenticationContext(_environment.AzureADTenant, TokenCache.DefaultShared);
            
            //generate the token
            Task<AuthenticationResult> task = context.AcquireTokenAsync(_environment.AzureResource, creds);
            
            //wait for the task to finish
            task.Wait();
            
            //return the result
            return task.Result;
        }


Both of the methods are called via
        public static string GetAzureAuthenticationHeader(AXEnvironments _environment)
        {
            AuthenticationResult result;
            string authHeader = "";

            try
            {
                //generate the token and get an authorization
                result = GenerateAzureToken(_environment);
                authHeader = result.CreateAuthorizationHeader();
            }
            catch
            {
                authHeader = "";
            }

            return authHeader;
        }

Which is passed to
string authHeader = D365Auth.AXAuthorizationDefined.GetAzureAuthenticationHeader(currentEviornment);

which is sent to the http request header.


Just for reference if you do want to use a defined username and password you can use the following

        public static AuthenticationResult GenerateAzureToken(AXEnvironments _environment)
        {

            //create the credential object based upon the stored username/password
            UserPasswordCredential credential = new UserPasswordCredential(_environment.UserName, _environment.Password);
            
            //setup the context of the token
            AuthenticationContext context = new AuthenticationContext(_environment.AzureADTenant, TokenCache.DefaultShared);
            
            //generate the token
            Task<AuthenticationResult> task = context.AcquireTokenAsync(_environment.AzureResource, _environment.AzureClientAppId, credential);
            
            //wait for the task to finish
            task.Wait();
            
            //return the result
            return task.Result;
        }




This post assumes you have already setup authenication in Azure and D365FO which is gone over @ D365FO - Azure authentication setup for web services (defined creds and interactive login)






Azure - Delete button on native application registration is disabled / cannot remove

While working on figuring out how authentication between Azure <--> D365FO <--> Custom web service works I found myself unable to delete one of the application registrations I created because the delete button was disabled for the native application I created but not for the web api registration I created.


In order to fix this we need to do the following:



1: Click on manifest -> Edit

2: Change the property 'availableToOtherTenants' from true to false




3: Hit Save


The delete button should now be enabled.




If you have the same issue with a registered app that is of type Web app/ API then you can use the GUI to update this property by going to Settings > Properties > Multi-tenanted and simply change from yes to no.




D365FO - Azure authentication setup for web services (defined creds and interactive login)

When developing web services within D365FO for other applications/languages we need to follow a new authentication process compared to AX 2012 since everything is hosted on Azure. The following will show you the settings that need to be defined within your Azure dashboard and within D365FO to enabled authentication to execute a custom web service (SOAP/JSON) or Odata calls.

It is good to note that you may need to setup up a webapi or a native application depending on what you are trying to accomplish. Both are about the same just the native application does not require the key generation.

Before I go over the steps needed the following goes over the multiple types of authentication for Azure and explains the difference between Native vs Web API auth scenarios: https://docs.microsoft.com/en-us/azure/active-directory/develop/authentication-scenarios 


Azure Setup for defined credentials/web api

Register a web app / api

Step 1: In the Azure port go to Azure Active Directory > App registrations > New Application registration

Step 2: Enter in the environment information for D365 and hit create

Step 3: Once initialized click on the settings button


Step 4: Click on required permissions > add > Select API


Step 5: Choose "Microsoft Dynamics ERP"

Select which options you want to give it access to. (usually all of them)

Choose done.

Step 6:  choose "keys"

 Under password input a description and choose when the cert should expire






Hit the save button and save the "value" aka the key. This is what will be used as a "handshake"


Step 7 (optional): Do the same thing but for a native application (interactive login). Just enter the main login url as the redirect url. You will not need the Key generation part.



D365FO setup:

Step 8 : In D365FO go to System administration > Setup > Azure Active Directory applications and create a new record for the app registration with the client id(s) from the Azure setup. If you are using the defined cred's via the web app / api type as listed below the User Id listed in this screen is what the system will log an new records created via the webservice








Monday, August 6, 2018

D365FO - TableGroupAll dynamic lookup based on previous field example

The following is an example of how to add in a lookup field to a form that will change sources of the lookup depending on a previous selection. In the example below we a mandatory field that has to be filled in before it can be inserted into a table however the field that needs to be filled in will be populated by a dropdown from either CustomCustomerAccountView or CustomCustomerGroupsView which is determined by a previous field in the datasource. The value that will be saved will be of two different edt types or blank but saved within the same field. This utilizes the idea behind the default Enum TableGroupAll which allows the user to select a Table, GroupId or All as an enum value and then the user selects something that is of type table(specific), group or all.

Our detailed scenario:

Form Details:
Custom form called MyCustomForm which we need need to look at the control MyCustomForm_AccountCode to determine what the lookup for MyCustomForm_AccountSelection will show.

Table Details:
Table: MyCustomRuleTable  - this is where the value of AccountCode(enum TableGroupAll) and AccountSelection (lookup value) will be saved

Lookup Details:
TableGroupAll::Table = pull the data from CustomCustomerAccountView and save the customer account id
TableGroupAll::GroupId = pull the data from CustomCustomerGroupsView and save the customer group id
TableGroupAll::All = do not allow the user to select anything and the value should be blank

This field will be in a grid value so we need to also determine when to enable/disable the selection field as well as marking the field as mandatory.



[ExtensionOf(formStr(MyCustomForm))]
final class MyCustomForm_Extension
{
    /// <summary>
    /// Lookup code for account selection based on TableGroupAll selection
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    [FormControlEventHandler(formControlStr(MyCustomForm, MyCustomForm_AccountSelection), FormControlEventType::Lookup)]
    public static void MyCustomForm_AccountSelection_OnLookup(FormControl sender, FormControlEventArgs e)
    {
        Query query = new Query();
        QueryBuildDataSource queryBuildDataSource;
        SysTableLookup sysTableLookup;
        FormRun formRun;
        FormControl formControl;
        TableGroupAll accountCodeType;

        //get the value the account code selection
        formRun = sender.formRun();
        formControl = formRun.design().controlName(formControlStr(MyCustomForm, MyCustomForm_AccountCode));

     

        // check to see what the value of the account code field is to determine what lookup table we should use
        if(str2Enum(accountCodeType, formControl.valueStr()) == TableGroupAll::Table)
        {
            //look up that shows the customer number and name
            sysTableLookup = SysTableLookup::newParameters(tableNum(CustomCustomerAccountView),sender,true);
            sysTableLookup.addLookupfield(fieldNum(CustomCustomerAccountView, AccountNum),true);
            sysTableLookup.addLookupfield(fieldNum(CustomCustomerAccountView, Name));

            queryBuildDataSource = query.addDataSource(tableNum(CustomCustomerAccountView));
            queryBuildDataSource.addSortField(fieldnum(CustomCustomerAccountView,AccountNum));

            sysTableLookup.parmQuery(query);
            sysTableLookup.performFormLookup();
        }
        else if(str2Enum(accountCodeType, formControl.valueStr()) == TableGroupAll::GroupId)
        {
            //lookup that shows the smart atp group id and descriptions
            sysTableLookup = SysTableLookup::newParameters(tableNum(CustomCustomerGroupsView),sender,true);
            sysTableLookup.addLookupfield(fieldNum(CustomCustomerGroupsView, GroupCodeId),true);
            sysTableLookup.addLookupfield(fieldNum(CustomCustomerGroupsView, GroupCodeDescription));

            queryBuildDataSource = query.addDataSource(tableNum(CustomCustomerGroupsView));
            queryBuildDataSource.addSortField(fieldnum(CustomCustomerGroupsView,GroupCodeId));

            sysTableLookup.parmQuery(query);
            sysTableLookup.performFormLookup();
        }

        //cancel super() to prevent error.
        FormControlCancelableSuperEventArgs ce = e as FormControlCancelableSuperEventArgs;
        ce.CancelSuperCall();
    }
 
    /// <summary>
    ///  Selecting the current record from the forms data source
    /// </summary>
    /// <param name="sender">Forms data source as customtablename</param>
    /// <param name="e">Data source event</param>
    [FormDataSourceEventHandler(formDataSourceStr(MyCustomForm, MyCustomRuleTable), FormDataSourceEventType::Activated)]
    public static void MyCustomRuleTable_OnActivated(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormDataSource formDS = sender.formRun().dataSource(formDataSourceStr(MyCustomForm, MyCustomRuleTable));
        MyCustomRuleTable currentRule = formDS.cursor();
        boolean enableAccountSelection;


        //if the parent code is currently set to all then we should not allow the user to select the child selection field
enableAccountSelection = currentRule.AccountCode == TableGroupAll::All ? false : true;


//apply the allow edit/mandatory logic checks
formDS.object(fieldNum(MyCustomRuleTable, AccountSelection)).allowEdit(enableAccountSelection);
        formDS.object(fieldNum(MyCustomRuleTable, AccountSelection)).mandatory(enableAccountSelection);
     }

    /// <summary>
    /// Account code modified event
    /// </summary>
    /// <param name="sender">MyCustomRuleTable.AccountCode</param>
    /// <param name="e">Event args</param>
    [FormDataFieldEventHandler(formDataFieldStr(MyCustomForm, MyCustomRuleTable, AccountCode), FormDataFieldEventType::Modified)]
    public static void AccountCode_OnModified(FormDataObject sender, FormDataFieldEventArgs e)
    {
        FormDataSource formDS = sender.datasource();
        MyCustomRuleTable currentRule = formDS.cursor();
        boolean enableAccountSelection;

//clear the current account selection anytime the account code is changed
        currentRule.AccountSelection = "";

        //if the parent code is currently set to all then we should not allow the user to select the child selection field
enableAccountSelection = currentRule.AccountCode == TableGroupAll::All ? false : true;

        //apply the allow edit/mandatory logic checks
        formDS.object(fieldNum(MyCustomRuleTable, AccountSelection)).allowEdit(enableAccountSelection);
        formDS.object(fieldNum(MyCustomRuleTable, AccountSelection)).mandatory(enableAccountSelection);
    }
}

D365FO - Extension method data accessor examples


Six months into learning how to transition from AX 2012 X++ to D365FO X++ one of the things I have been struggling with the new extension model is how many different code structures you need in order to access the calling method’s property’s/datasource when subscribing to various types of events or methods. So I started to log down anytime I discover a new method. As I am sure there are plenty of other ones that I am missing as I have only touched the tip of the iceberg when it comes to field events but I decided it would be a good time to post this information as a reference. There is more than likely better ways to do some of these however I am posting this as a starting point.

I will be updating this post as my experience with D365FO grows as well

Source Event Parm Example
Class Pre/Post event XppPrePostArgs Get args and parmameter values from a method that is being extended. Parm 1 = Object Parm 2 = Common
PurchCreateFromSalesOrder callingClass = args.getThis() as PurchCreateFromSalesOrder;       
Object callerObject = args.getArgNum(1) as Object;
Common callerRecord = args.getArgNum(2) as Common;
Class Pre/Post event XppPrePostArgs Class example: SalesLineType salesLineType = args.getThis() as SalesLineType; 
Class main args Getting the caller record that is sent to a class via args. This is the same as 2012 however args.record().datasource() is now deprecated
//check the caller
if(_args.callerName() == formStr(SalesTable))
{
        //check to see which dataset type was passed
        if(_args.dataset() == tableNum(SalesLine))
        {
                //get the forms datasource record (FormDataUtil::getFormDataSource() replaced _args.record().datasource())
               FormDataSource salesLineDS = FormDataUtil::getFormDataSource(_args.record());
        }
}
Form Initialized xFormRun FormDataSource purchLine = sender.dataSource(formDataSourceStr([formname],[table]));
Form DataSource FormDataSource FormDataSource formDS = sender.formRun().dataSource(formDataSourceStr(EcoResProductDetailsExtended, MHSmartATPItemSettings));
MHSmartATPItemSettings smartATPItemSettings = formDS.cursor();
Form DataSource Field FormDataObject FormDataSource formDS = sender.datasource();
PurchLine purchLine = formDS.cursor();
Form Form Control FormControl FormRun formRun;
FormControl formControl;
formRun = sender.formRun();
formControl = FormRun.design().controlName(formControlStr(<form name>, <control name>));
someVariable = formControl.valueStr();
Form onClicked FormControl  FormRun formRun = sender.formRun();
 FormDataSource formDSSalesTable = formRun.dataSource(formDataSourceStr(SalesTable, SalesTable));
 FormDataSource formDSSalesLine = formRun.dataSource(formDataSourceStr(SalesTable, SalesLine));
       
 SalesTable salesTable = formDSSalesTable.cursor();
 SalesLine salesLine = formDSSalesLine.cursor();
Form Pre/Post event XppPrePostArgs FormRun formRun = args.getThis();
FormDataSource formDSLogisticsPostalAddress = formRun.dataSource(formDataSourceStr(LogisticsPostalAddress, LogisticsPostalAddress));
LogisticsPostalAddress logisticsPostalAddress = formDSLogisticsPostalAddress.cursor();
Table onDelete Common PurchLine purchLine = sender as PurchLine;
Table Modified Field Value Common TableName itemSettings = sender as TableName;
ModifyFieldValueEventArgs fieldEvent = e as ModifyFieldValueEventArgs;

  //check to see which field was modified
  switch(fieldEvent.parmFieldName())
        {
            case fieldStr([tablename], [fieldname]):
            ...do stuff
            break;
        }
Table ValidateFieldValue Common/DataEventArgs ValidateFieldValueEventArgs fieldEvent = e;
boolean isValid;
PurchLine purchLine = sender as PurchLine;
       
//declare the checkFailed      
isValid = checkFailed("some error event");
//save the result
fieldEvent.parmValidateResult(isValid);
Table Pre/Post event XppPrePostArgs PurchLine purchLine = args.getThis() as PurchLine;
Form Getting different datasource from calling object FormDataObject Get different datasource from formdataobject. Such as modified InventDim.InventLocationId -> PurchLine
 FormDataSource formDS = sender.datasource();
InventDim inventDim = formDS.cursor();
FormRun formRun = sender.datasource().formRun();
FormDataSource formPurchLineDS = formRun.datasource(formDataSourceStr(PurchTable, PurchLine));
PurchLine purchLine = formPurchLineDS.cursor();

It is good to note that when it comes to accessing anything on a form I have realized the key component is getting access to the FormRun object. Once you have access to that then you can really access anything that is public on the form such as controls or datasources

Update 8/7/18: added class args example as args.record().datasource() is now deprecated
Update 1/23/19: added getting different datasource example than the sender's main common source