Dynamics 365 Business Central and direct printing

This seems to be one of the hottest “problems” when using Dynamics 365 Business Central SaaS: actually you cannot directly print a document on a printer, but you need to open the PDF file and then print from the browser.

This could be ok for documents or for other not so repetitive tasks, but it could be a problem on scenarios like warehouse or production and so on, where you need to send to a printer a big amount of documents.

Unfortunately there’s no an “out of the box” solution for that and Microsoft at the moment has no plans for implementing that feature quite soon. So, possible solutions for this hot topic? I don’t have THE ANSWER to this question, but only some ideas to share.

I have personally tested some possible solutions:

Solution 1: using a commercial tool called Printnode. This is a remote printing service for web applications that expose an HTTPS API that you can call for creating print jobs via HTTP calls. You need to sign up for the service and install a local client.

To create a print job with PrintNode, you have to create a POST request to the printjobs endpoint. More informations here.

If you want to use PrintNode from C#, I suggest you to use this wrapper available on GitHub. With this library, you can create a print job from C# using the following code:

//Sets a specific printer by passing the ID of the registered printer in PrintNode
var printerId = 38409; 
var printer = await PrintNodePrinter.GetAsync(printerId);

//Add a job to a printer
byte[] pdfDocument = GetPDFStream(YourPDFUrl);
var printJob = new PrintNodePrintJob
{
   Title = "Document Print",
   Content = Convert.ToBase64String(pdfDocument),
   ContentType = "raw_pdf"
};

var response = await printer.AddPrintJob(printJob);

Solution 2: using Google Cloud Print service. Google Cloud Print is a service that permits you to add your WiFi-enabled printers under your Google accounts and from here you can send print jobs from the web.

Adding a local printer to Google Cloud Print is really easy:

  1. Connect your local printer to your WiFi network
  2. Open Chrome and type chrome://devices, then press enter
  3. Search for your printer and click Register.
  4. If you then go to https://www.google.com/cloudprint#printers you should see your registered printers:

d365bcdirectprinting_02

Now you can use your printer from the web. You need to authenticate yourself to Google Cloud Print and then you can use the APIs to send print jobs. More informations can be found here.

For this solution, I’m using the exact steps I’ll describe in the next section (Solution 3). The step 3 is the only different. Fo this solution, I have a service that reads the document to print from the storage and then creates a Google Cloud Print job.

For this task, I’m using a .NET library (wrapper) available on GitHub. With this library, we can generate an access token to our local machine by authenticating with Google Cloud Print via OAuth2:

var provider = new GoogleCloudPrintOAuth2Provider(clientId, clientSecret);

// You should have your redirect uri here if your app is a server application, o.w. leaving blank is ok
var url = provider.BuildAuthorizationUrl(redirectUri);

/* Your method to retrieve authorization code from the above url */
var token = await provider.GenerateRefreshTokenAsync(authorizationCode, redirectUri);

Then we can submit a print job for a document in the following way:

private static void SubmitJob(string documentURL, string printerID)
{
   var client = new GoogleCloudPrintClient(provider, token);
   string path = documentURL;

   Printer printer = new Printer();
   printer.Id = printerID;

   var cjt = new CloudJobTicket
   {
      Print = new PrintTicketSection
      {
         Color = new ColorTicketItem { Type = Color.Type.STANDARD_MONOCHROME },
         Duplex = new DuplexTicketItem { Type = Duplex.Type.LONG_EDGE },
         PageOrientation = new PageOrientationTicketItem() { Type = PageOrientation.Type.LANDSCAPE },
         Copies = new CopiesTicketItem() { Copies = 1 }
      }
   };

   JobResponse<SubmitRequest> response;

   var request = new SubmitRequest
   {
      PrinterId = printer.Id,
      Title = Guid.NewGuid().ToString(),
      Ticket = cjt,
      Content = new SubmitFileLink(path)
   };
   response = client.SubmitJobAsync(request).Result;

   Console.WriteLine("Print Job submission Success: {0}", response.Success);

}

The main problem with this solution is the token expiration time.  A token expires after 60 minutes and you need to recreate a new token after that time.

Solution 3: implement your custom direct printing solution! Actually I’m testing an “home made” solution for direct printing from D365BC SaaS. Basically, the solution is composed of three main parts:

  1. Code from AL for saving the report as a PDF stream
  2. Azure Function for saving the stream to a local storage
  3. Custom service for printing the document to the local printer

The schema of this solution is represented in the following diagram:

D365BCDirectPrinting_01.jpg

The report streaming is handled in AL code by calling a function that takes the report parameters, executes it and saves the report as a stream and then calls an Azure Function by passing the Base64 stream of this report. The AL code is as follows:

procedure StreamReport(): Text

var

   outStreamReport: OutStream;

   inStreamReport: InStream;

   Parameters: Text;

   tempBlob: Record TempBlob temporary;

   Base64EncodedString : Text;

begin

   Parameters := Report.RunRequestPage(Report::MySalesOrder);

   tempBlob.Blob.CreateOutStream(outStreamReport, TextEncoding::UTF8);

   Report.SaveAs(Report::MySalesOrder, Parameters, ReportFormat::Pdf, outStreamReport);

   Base64EncodedString := tempBlob.ToBase64String();

  exit(Base64EncodedString);

end;

Then, we create a JSON request content to our Azure Function and we perform a POST request to that function. The pseudo-code we use is as follows:

procedure CallAzureFunctionPrinting(Base64EncodedString: Text; filename: Text; fileExt: Code[10])

var

   httpClient: HttpClient;

   httpContent: HttpContent;

   jsonBody: text;

   httpResponse: HttpResponseMessage;

   httpHeader: HttpHeaders;

begin

   jsonBody := ' {"base64":"' + Base64EncodedString + ',"fileName":"' + filename +

                           '","fileType":"' + fileExt + '"}';

   httpContent.WriteFrom(jsonBody);

   httpContent.GetHeaders(httpHeader);

   httpHeader.Remove('Content-Type');

   httpHeader.Add('Content-Type', 'application/json');

   httpClient.Post('MyFunctionUrl/StoreDocument', httpContent, httpResponse);

end;

 

The Azure Function is an HttpTrigger function (you can invoke it via an HTTP request). The Azure Function is defined as follows:

[FunctionName("StoreDocument")]
public static async Task<HttpResponseMessage> Run(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequestMessage req)
{
   dynamic data = await req.Content.ReadAsAsync<object>();
   string base64Data = data.base64;
   string fileName = data.fileName;
   string fileType= data.fileType;
   string fileExt = data.fileExt;
   Uri uri = await UploadBlobAsync(base64Data,fileName,fileType,fileExt);
   return req.CreateResponse(HttpStatusCode.Accepted, uri);
}

The function receives a JSON request as input (Http POST), reads the content and extract the data on it (the base64 string of the document, the file name, type and extension).

Then it calls a method (UploadBlobAsync) for uploading the file to the Azure Storage (I normally use a dedicated container called printerqueue. See a container as a sort of “directory” inside your Blob Storage account). Code for this method is omitted here (very simple).

As last step, on the client side (internal network) we have a Windows Service that every N seconds polls the Azure Blob Storage (printerqueue container) for checking if there are documents to print. If so, it reads the document and sends it to the printer.

This service has the following main code:

List<string> filesToPrint = DownloadFilesToPrintFromBlobStorage();
foreach(string filename in filesToPrint)
{
   SendToPrinter(filename);
}

It downloads the file from the Azure Storage and saves them to a local repository. Then, for every file it calls the SendToPrinter function that performs the printing.

I have N versions of the SendToPrinter function, but what performs well is the “bad method”, alias using ProcessStartInfo and calling the print verb:

private static void SendToPrinter(string filename)
{
   ProcessStartInfo info = new ProcessStartInfo();
   info.Verb = "print";
   info.FileName = filename;
   info.CreateNoWindow = true;
   info.WindowStyle = ProcessWindowStyle.Hidden;

   Process p = new Process();
   p.StartInfo = info;
   p.Start();

   p.WaitForInputIdle();
   System.Threading.Thread.Sleep(3000);
   if (false == p.CloseMainWindow())
      p.Kill();
}

The document is printed on the default printer associated to the server.

This solution is absolutely not perfect and it can be improved for sure. The scope is here was only to give you ideas and tips on how to implement your own direct printing solution.

As said before, I think that Microsoft needs to work on this topic with an high priority. Printing in an ERP system is a top requested feature and it could be a SaaS-killer reasonfor many scenarios if not available.

I don’t think this is so difficult to do for Microsoft. They could implement a service that must be installed locally that receives the document stream from Dynamics 365 Business Central and print it on a local printer. This function could be native on every report when a user clicks on “Print directly”.

There’s an idea submitted for that, if you want to support it I think it could tune up the priority with the Product Team

Comment List
Related
Recommended