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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
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:
1 2 3 4 5 |
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:
1 2 3 4 5 6 7 8 9 |
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:
1 2 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
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:
1 2 3 4 |
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:
1 2 3 4 5 6 |
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.
Finally
Well, I hope anyone can use this or has learned at least something out of this.
Enjoy!
Pingback: From the Microsoft Dynamics NAV Blogs: PDF printing in Classic; Automated patching; Company backup and restore; Alternative production BOMs - MSDynamicsWorld.com - NAV - Dynamics User Group
Pingback: Printing PDF Reports in Classic without external components | Pardaan.com
If there was a NAV Hack of the Month, I would vote this!
Thanks!
I’d like to reminder everyone, that there are no explicit credentials set up in this code currently. This means that the current user windows login is used to verify against the NST.
If the customer uses database logins, THAT WILL NOT WORK!
You need to at least add their users as windows logins, or change the code to pass certain credentials.
Can you please tell me more of
“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 :)”
because I’m exactly in the same case. Which functions have you merged in the codeunit1 ?
I believe the functions below should be enough. You should copy them from a 2009R2 codeunit 1 as well, so you don’t loose the ControlID’s.
– DefaultRoleCenter
– ValidateApplicationLanguage
– LookupApplicationLanguage
I just tried to start the WebServices and checked the event viewer to see which function I needed.
What should webservice do? That code must have?
Thanks.
Hi,
The webservice should be able to use the SaveAsPDF on a report, where the classic client cannot. This way, the webservice will generate the PDF and send it back to the classic client through the webservice.
We must create a new webservices to use “PrintPDF using webservice Call” codeunit
“baseURL: = STRSUBSTNO (TextWS, MySetup “Webservice Server Name”, MySetup “Webservice Server Port”, MySetup “Webservice Server Instance”). ”
and sets it on the MySetup table, right?
There is no longer created for NAV?
Thank
good afternoon
Thank you for this excellent development.
I tried a Report Layout and works perfectly with the Classic Client.
I tried without a Report Layout and generates an error reporting:
Error Executing WS:
T
he layout of this report has not been trasformed to be viewed in the role Tailored client. You can run this report from the Microsoft dynamics NAV Classic Client.
I’m running it from Classic Client,
The reports need to have layout?
What is the problem?
best regards
Hello Alvaro,
The reports need to have an RDLC layout for this to work.
Thank You
Do you know any way to do this without Layout to reports from RTC?
Conoces alguna manera de realizarlo para reports sin Layout desde RTC?
Best Regards
Hello Alvaro,
The saveAsPDF only works on RDLC Reports. If you need PDF from classic layout reports, you’ll need to look into an external component. If you are trying to start this from the RTC, there is, as far as I know, no way to do this.
Regards