Sales Document Preview

As promised, it is time to share an enhancement for the document posting preview.

The posting preview functionality in NAV 2016 is built around a single-instance Codeunit number 19.  That Codeunit has temporary tables declared as global variables and what ever is written to these tables remains there even if the actual database write is rollbacked.

The standard functionality displays a navigation page where the user can view the entries about to be posted.

Preview

So far so good.

Still, my customers have never asked for this feature.  They have, however, asked for the possibility to preview the document report.  That is what this blog post is about.

PreviewDocument

There are a few steps we need to follow in order to get there.  I will go through these steps.  If you want to cheat than just download the attached objects for NAV 2016 W1 CU2 and merge the changes into your database.

We will use the same method, that is, saving the document header and document lines in temporary tables and using them to feed the reports.  We will start by creating a copy of our sales invoice (206) and sales credit memo reports (207).  We need a new reports to handle temporary tables and we also want to make a few changes to make sure that it is clearly a preview report.

The changes we make are;

  • Change the Text Constant “Sales – Invoice %1” to “Sales – Invoice Preview”
  • In the Request Page, remove options for NoOfCopies and LogInteraction
  • Change DataItem “Sales Invoice Header” and “Sales Invoice Line” to be temporary
  • Clear ReqFilterHeadingML and ReqFilterFields properties for DataItem “Sales Invoice Header”
  • Add a public function “SetProperties” with parameters for the temporary tables and use the COPY function to copy the temporary table instance to the report Data Items.

SetProperties

Make the same changes to the credit memo report.

Now, lets look at Codeunit 19 and add what ever we need there.  It should be obvious that we need to add the global variables for our sales document tables.  We also add a boolean variable to change the mode from navigation to sales document preview.

Codeunti19Globals.PNG

Make sure that these temporary tables are emptied in initialization.

DeleteAllTempTables.PNG

Use the new EventSubscriber functionality to gather the document header and lines.

EventSubscribers

Externally we should be able to change the preview mode to a document preview mode.

SetDocumentPreviewMode.PNG

The last function we need is the one running the preview report.  We define our two new reports as a local variable and call them from this function.

RunReportPreview.PNG

The final step for Codeunit 19 is to make sure that the correct action is taken depending on our new boolean variable.

ShowAllEntries.PNG

We also need to make changes to Codeunit 81.  From there the Preview is started.  We need to make a change to the standard code to enable the document preview.  The rollback must be performed before calling the ShowAllEntries.  Rollback is done by using ASSERTERROR ERROR(”);

ErrorMoved.PNG

Almost done.  Another public function to Codeunit 81 is needed.  Here we change the preview mode before starting the default preview functionality.

StartPreviewDocument.PNG

Now, we can start adding actions to the sales pages.  For example in page 9305, Sales Order List.

  • Copy action 25
  • Rename to “Preview Document”
  • Change Image to PrintDocument
  • Add Codeunit 81 as a local variable in the action C/AL code
  • Add C/AL code SalesPostYesNo.PreviewDocument(Rec);

Pressing that new action will bring up this request page.

RequestPage.PNG

Pressing Preview should give you a good preview of how the printed document will look like after posting.

PreviewSalesInvoice.PNG

Download DocumentPreview and start playing.

Report upgrade from 2009 to 2013

I have noticed within the group of consultants and developers that the move to Visual Studio to handle report layout is quite a leap.  Not many are courageous enough to make this leap.

One of the most used tricks in the new layout is how headers and footers are handled.  To get the correct data up to the header og down to the footer a method using Visual Basic Code is used.

In our payroll system upgrade from NAV 2009 to NAV 2013 all reports had been upgraded.  However, they did not work properly and so the problem ended on my desk.

I found the problem and of course Claus had already blogged about this change of functionality between NAV 2009 and NAV 2013.

“The textbox is hidden and needs to be first thing that activated when the report is rendered. So hold that thought, it has to be hidden and at the top for Code.GetData to work. In NAV 2009 reports are rendered in Report Viewer 2008 this works like a charm, but in Report Viewer 2010 the SSRS team has been so kind to us to change the logic of how a reports is rendered. So if an textbox is hidden in RDLC 2008, the expression in this hidden textbox, is rendered at the end. So as I said the SetData textbox has to be hidden and at the top for Code.GetData to work, but now it is rendered as the last thing, and of course the result is that the fields get out of sync and the very idea using Code.GetData and Code.SetData is broken in NAV 2013 with Report Viewer 2010. Interesting thought!!! “

The first thing I needed to do was to add a single line of code to the Code.SetData function.  Go to Report Properties to solve this.

ReportProperties

The “Return True” line makes sure that the field is visible and therefore rendered at the top.

Next to change the text box with the Code.SetData function.  In NAV 2009 layout we called this function by using it as an expression and set visibility to hidden.

ReportExpression

In NAV 2013 layout clear the expression and move the Code.SetData function to the Visibility Section.

ReportVisibility

And that’s it folks…

Use SAVEASPDF to E-Mail Invoices and Credit Memos

When I was running NAV 2009 I used PDFCreator, BioPDF or BullZipPDF printers to create a PDF copy of a report.  In NAV 2013 we have the option to use native SAVEASPDF command that uses built-in methods to create a PDF document on the server.

I used the Job Queue in NAV 2009 to send all my invoices and credit memos via email to my customers.  An updated version for NAV 2013 that uses

[code]
FileName := FileMgt.GetDirectoryName(FileMgt.ServerTempFileName(‘pdf’)) + ‘\’ + STRSUBSTNO(‘%1 %2.pdf’,TABLECAPTION,"No.");
IF EXISTS(FileName) THEN
ERASE(FileName);
SalesInvHeader := "Sales Invoice Header";
SalesInvHeader.SETRECFILTER;
CLEAR(Invoice);
Invoice.SETTABLEVIEW(SalesInvHeader);
Invoice.USEREQUESTPAGE(FALSE);
IF Invoice.SAVEASPDF(FileName) THEN BEGIN[/code]

is attached below.

JobQueueEMailer2013

Send multiple base 64 encoded files to a single Web Service

Sometimes a little more flexibility is needed than a single XML Port in a web service function.  Then it is possible to send multiple files to a single web service.  I am working on an enhancement for the inter-company posting feature in Dynamics NAV.

To add to the standard functionality I want to be able to use web services to deliver an invoice from one company to another.  I send two XML files and two PDF files to web service.

In the Classic Client I use then ‘CG Request Client’.Base64 to encode the files into a XML node.

[code]
XMLElement3.appendChild(XMLNode);
XMLNode := XMLRequestDoc.createElement(‘pDFInvoice’);
IF "PDF Document".HASVALUE THEN BEGIN
PDFInvoiceFileName := FileMgt.ClientTempFileName(”,’pdf’);
ICOutboxTrans."PDF Document".EXPORT(PDFInvoiceFileName,FALSE);
Base64.Encode(PDFInvoiceFileName,XMLNode);
ERASE(PDFInvoiceFileName);
END;
XMLElement3.appendChild(XMLNode);[/code]

in the Role Tailored Client I use dotnet interop

[code]
XMLNode4 := XMLRequestDoc.CreateElement(‘pDFInvoice’);
IF "PDF Document".HASVALUE THEN BEGIN
"PDF Document".CREATEINSTREAM(InStr);
DOWNLOADFROMSTREAM(InStr,Text006,Path,Text009,PDFInvoiceFileName);
XMLNode4.InnerText := Convert.ToBase64String(ClientFile.ReadAllBytes(PDFInvoiceFileName));
ClientFile.Delete(PDFInvoiceFileName);
END;
XMLElement3.AppendChild(XMLNode4);[/code]

On the service part it is possible to write the file to a BLOB without using a temporary file.

[code]
IF PDFInvoice.LENGTH > 0 THEN BEGIN
Bytes := Convert.FromBase64String(PDFInvoice);
MemoryStream := MemoryStream.MemoryStream(Bytes);
ICInboxTransaction."PDF Document".CREATEOUTSTREAM(OutStr);
MemoryStream.WriteTo(OutStr);
END;[/code]

bioPDF writer

I have been using PDFCreator to create PDF documents from my NAV client.  I am also using it with NAS (Nav Application Server) on my Windows 2003 32bit server.  I needed to get this up an running on a 64bit Windows 2008 server and I ran into problems.

I could easily create the PDF documents from my client.  I saw the pdf file appear in my file system and attached it to my record.  But, when NAS was running the same job no files where created and the job failed.  I unsuccessfully tried a few tricks but nothing changed.  I decided to find another path and looked at bioPDF writer.

The result is a new single instance codeunit to handle the print of any report to a pdf file.  Lets say that the user would like to be able to print and open a sales invoice in pdf format.  Copy the function PrintRecords to PrintRecordsToPDF for table no. 112, Sales Invoice Header.  Add two lines and you are done.

[code htmlscript=”false”]WITH SalesInvHeader DO BEGIN
COPY(Rec);
FIND(‘-‘);
ReportSelection.SETRANGE(Usage,ReportSelection.Usage::"S.Invoice");
ReportSelection.SETFILTER("Report ID",'<>0’);
ReportSelection.FIND(‘-‘);
REPEAT
bioPDFMgt.BeforeReportPrint(ReportSelection."Report ID"); // Dynamics.is
REPORT.RUNMODAL(ReportSelection."Report ID",ShowRequestForm,FALSE,SalesInvHeader);
HYPERLINK(bioPDFMgt.AfterReportPrintGetFileName(ReportSelection."Report ID")); // Dynamics.is
UNTIL ReportSelection.NEXT = 0;
END;[/code]

Where bioPDFMgt is a local variable for the BioPDF Management Codeunit.

The BioPDF Management Codeunit needs to be added to global variable in Codeunit 1, ApplicationManagement.  Add to the FindPrinter function.

[code htmlscript=”false”]FindPrinter(ReportID : Integer) : Text[250]
// Dynamics.is start
IF bioPDFMgt.PrinterBufferExists(ReportID) THEN
EXIT(bioPDFMgt.GetPrinterName);
// Dynamics.is end

CLEAR(PrinterSelection);

IF NOT PrinterSelection.GET(USERID,ReportID) THEN
IF NOT PrinterSelection.GET(”,ReportID) THEN
IF NOT PrinterSelection.GET(USERID,0) THEN
IF PrinterSelection.GET(”,0) THEN;

// Dynamics.is start
bioPDFMgt.SaveLastPrinter(PrinterSelection."Printer Name");
// Dynamics.is end

EXIT(PrinterSelection."Printer Name");[/code]

The BioPDF Management Codeunit includes these functions:

Name Description
BeforeReportPrint Setup PDF printer and printer selection
AfterReportPrintGetBLOB Copy the PDF file to a tempBLOB record
AfterReportPrintGetFileName Return the PDF file name
CleanUp Clear PDF automation objects
ConfirmFileExists Return an error if file does not exist
FileExists Return a boolean value for given file name
FileRename Rename a file (copy and delete combined)
FileErase Delete a file
FileCopy Copy a file
ClearPrinterBuffer Clear the printer selection buffer
CreatePrinterBuffer Adds a report and a printer name to the printer selection buffer
PrinterBufferExists Check if a printer selection buffer exists for given report
GetPrinterName Get the printer name from the printer selection buffer
SaveLastPrinter Save the name of the last printer used
GetLastPrinter Get the name of the last printer used

BioPDF Management Codeunit (2009 R2)

BioPDF Management Codeunit (5 SP1)

E-Mailing PDF Report with Job Queue

One of my clients asked for a automated report delivery every morning.  Since Job Queue was already running I decided to create a way to have Job Queue print the report to PDF and email it as an attachment.

This example uses the E-Mailer Report no. 50003 to send Report No. 6 to my email address.

First I had to make some changes to codeunit 449, Job Queue Start Codeunit, the OnRun trigger.  I created a local variable with the name JobQueueEntry as a Record 472 and then added to the code.
[code htmlscript=”false”]CASE "Object Type to Run" OF
"Object Type to Run"::Codeunit:
CODEUNIT.RUN("Object ID to Run",Rec);
"Object Type to Run"::Report:
//#01
IF "Parameter String" <> ” THEN BEGIN
JobQueueEntry := Rec;
JobQueueEntry.SETRECFILTER;
REPORT.RUN("Object ID to Run",FALSE,FALSE,JobQueueEntry);
END ELSE
//#01
REPORT.RUN("Object ID to Run",FALSE);
END;[/code]
Attached is report 50003 that does the job.

Any Report E-Mailer