web
You’re offline. This is a read only version of the page.
close
Skip to main content

Notifications

Announcements

No record found.

Community site session details

Community site session details

Session Id :
Dynamics 365 Community / Blogs / Magno's Blog / Printing PDF Reports in Cla...

Printing PDF Reports in Classic without external components

Community Member Profile Picture Community Member

Ever had a customer who asked to email his invoice in PDF, when they do not have / use RTC and work in the Classic Client? The answer is probably yes!

How did you solve this? Most probably you used an external component or PDF printer, and hoped that it would print and you could sent the correct PDF file?

Well, this question came to me again recently. The client had a 3.60 database (yeah, the old form menu), but was technical already on a 2009 R2 version.

Before I start: The exported files are attached to this post. Scroll to the bottom to find them.

You know you can save report as PDF on the RTC client, but not on the Classic Client. However, there was no NAV Service installed for the Client. After checking their license, I saw that they did have permission to run a Dynamics NAV Server.

But how to do this, without needing to redesign all your forms into pages and letting the user work on the Role Tailored Client? WEB SERVICES…

Right, the web service is also executed on the NAV Server, so it is able to save report as PDF.

Installing the web service Service

First, we installed the web service Service. Since it was a 3.60 database, I copied all system tables from a NAV2009R2 default database. Next, I had to merge a few functions into codeunit 1 to be able to start the web service Service. But this probably doesn’t matter for most of you. I’m sure you’ll get it running :)

Creating the web service codeunit

Next, I created a codeunit with 1 function: PDF Generator WS

GenerateReport(filter : Code[20];reportID : Integer;sourceTableID : Integer;VAR pdfFile : BigText)
FileName := TEMPORARYPATH + 'temp.pdf';

GLOBALLANGUAGE := 2067; //always print NLB

CASE sourceTableID OF
  DATABASE::"Sales Invoice Header":
    BEGIN
      lrecSIH.SETFILTER("No.", filter);
      REPORT.SAVEASPDF(reportID,FileName,SalesInvoiceHeader);
    END;
  DATABASE::"Sales Cr.Memo Header":
    BEGIN
      SalesCrMemoHeader.SETFILTER("No.", filter);
      REPORT.SAVEASPDF(reportID,FileName,SalesCrMemoHeader);
    END;
  ELSE
    ERROR('');
END;

CREATE(Document);
Element := Document.createElement('base64');
Element.dataType := 'bin.base64';

CREATE(Stream);
Stream.Type := 1;
Stream.Open;
Stream.LoadFromFile(FileName);
Element.nodeTypedValue := Stream.Read;
Stream.Close;
pdfFile.ADDTEXT(Element.text);

ERASE(FileName);

These are the variables:

Name	DataType	Subtype	Length
Document	Automation	'Microsoft XML, v6.0'.DOMDocument60	
Element	Automation	'Microsoft XML, v6.0'.IXMLDOMNode	
Stream	Automation	'Microsoft ActiveX Data Objects 2.8 Library'.Stream	
FileName	Text		1024

Basically, I create the report PDF file, and convert it to a base64 string using a stream.
I also allowed it to print different kinds of reports, by using parameters to supply the ReportID, source table and filter.

Extending the filters to use SETTABLEVIEW or something, is also possible I guess, but that shouldn’t be too much effort.

Next, you notice that I change the GLOBALLANGUAGE to 2067, but this could be a parameter in the function, which you just pass to GLOBALLANGUAGE of the caller into.

Adding extra possible tables to filter, is just a matter of adding them to the select CASE.

Consuming the web service

Then, I added the codeunit to the web service table, and voila, I have my web service published. Now I just needed to consume it.

So, another codeunit to consume it:

First a function to set filters:

setFilters(ReportID : Integer;SourceTableID : Integer;Filter : Text[30];FileName : Text[30])
Filters := '' + Filter + '';
Filters += '' + FORMAT(ReportID) + '';
Filters += '' + FORMAT(SourceTableID) + '';
Filters += '';

FileName := FileName;

FiltersSet := TRUE;

Any place I wish to use this function, I first call the SetFilters function passing which report, source table and table filter I need. Extra parameter is for the final file name.

Filters is a global text variable, FiltersSet a global boolean variable.

Next a simple function, to retrieve the CreatedPDFPath:

getCreatedPDFFileName() : Text[1024]
EXIT(ResultPDFPath);

This function could be used after the creation to get the path to the temporary file.

Finally the onRun function:

OnRun()
IF NOT FiltersSet THEN
  ERROR(Text001);

MySetup.GET;

baseURL := STRSUBSTNO(TextWS,MySetup."Webservice Server Name",MySetup."Webservice Server Port",
                             MySetup."Webservice Server Instance");
PDFCreateServiceNS := TextPDFCreateServiceNS;

PDFCreateServiceURL := baseURL + COMPANYNAME + TextCodeunit;

IF PDFCreateService(nodeList) THEN
BEGIN
  node := nodeList.item(0);
  TempFile := TEMPORARYPATH + txtFileName;
  IF ERASE(TempFile) THEN;

  node.dataType := 'bin.base64';

  IF ERASE(TempFile) THEN;
  CREATE(Stream);
  Stream.Type := 1; // Binary mode
  Stream.Open;
  Stream.Write(node.nodeTypedValue);
  Stream.SaveToFile(TempFile);
  Stream.Close;
  CLEAR(Stream);

END;

ResultPDFPath := TempFile;

MySetup is a simple setup table, where I use the path, port and Instance of the web service.

The function calls the web service, waits for its return in the pdfFile and decodes the base64 into a file on the local Client.

Please note that in this version, I have sort of hardcoded the web service name into a global variable: /Codeunit/GeneratePDF, some as the NameSpace.

So if you try this yourself and get some errors, check that your web service is published as GeneratePDF (Case Sensitive), or that you change the global variables to your namespace / web service name.

These are the most important variables:

Name	DataType	Subtype	Length
nodeList	Automation	'Microsoft XML, v6.0'.IXMLDOMNodeList	
node	Automation	'Microsoft XML, v6.0'.IXMLDOMNode	
Stream	Automation	'Microsoft ActiveX Data Objects 2.8 Library'.Stream

The last function “InvokeNavWS”, I’m not going to post. This is a simple function I found on the internet.

Consuming the new functionality

Well, lastly, I need to add a menuitem to the Posted Sales Invoice Form. I added the following code to my trigger:

PrintPDFusingWebserviceCall.setFilters(REPORT::"My Report",DATABASE::"Sales Invoice Header","No.",'Your Invoice.pdf');
PrintPDFusingWebserviceCall.RUN;

Customer.GET("Bill-to Customer No.");

Mail.NewMessage(Customer."E-Mail",'Your Invoice','','',PrintPDFusingWebserviceCall.getCreatedPDFFileName,TRUE);

So, in my example, I generate a PDF of my invoice, and open outlook with the customer email filled in, and the PDF as attachment already added.

First time the service is started, the generation took about 20 seconds. All the other times, I got to my outlook popup in less than 5 seconds. So not too bad, right?

Watch out!

So, this is only needed when the user works in the classic client and you will need to create an RDLC layout for the report off course.

But it will wait for the generation, you are sure that you only add to the report what you need, and you need no extra components which aren’t installed by default.

Other uses for this is the application server. If you need to email reports as PDF using the application server (Pre NAV2013).

Application server is not yet Role Tailored, so this can be used as a workaround. You could update the codeunit on the client side to check if it is SERVICETIER, so it wouldn’t make a web service call if not necessary. Anyway, you get the point.

Exporting the code…

Below, you’ll find the text export of my 2 codeunits.

GeneratePDF
GeneratePDF
GeneratePDF.txt
Version: 1.0
9.8 KiB
2 Downloads
Details...

Finally

Well, I hope anyone can use this or has learned at least something out of this.

Enjoy!

Comments

*This post is locked for comments