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();
            }