Updated: How to log report usage

Updated: Now with links to both NAV 5.0 and NAV 2009 hotfixes

 /Claus Lundstrøm


In several partners meetings I have heard this requests over and over again.

Claus, when we are planning an upgrade of Classic reports to RDLC reports, we do not always know which reports are actually being used at the customer site. It would be great to be able to log which reports are used so we know exactly which reports we need to upgrade to RDLC.

Well, if you download below hotfix you will now have the capability to log report usage at a customer site.

Dynamics NAV 5.0: KB2575296

Dynamics NAV 2009: KB2558650
Find links to all NAV 2009 Platform Hotfixes here:
CustomerSource: Overview of Released Platform Hotfixes for Microsoft Dynamics NAV 2009 SP1 and Microsoft Dynamics NAV 2009 R2
PartnerSource: Platform Hotfixes for Microsoft Dynamics NAV 2009 SP1 and Microsoft Dynamics NAV 2009 R2


In the following steps I have outlined which steps you need to do to get this log file. Remember above hotfix is required to perform the following steps.

1. Create new table to be used for Log report usage:


OBJECT Table 50000 Report Log
Version List=CLAUSL;
{ 1 ; ;No. ;Integer ;AutoIncrement=Yes;
MinValue=1 }
{ 2 ; ;User ID ;Code50 ;TableRelation="User Role"."Role ID";
CaptionML=ENU=User ID }
{ 3 ; ;Report ID ;Integer ;CaptionML=ENU=Report ID }
{ 4 ; ;Report Name ;Text249 ;FieldClass=FlowField;
CalcFormula=Lookup(AllObjWithCaption."Object Caption" WHERE (Object Type=CONST(Report),
Object ID=FIELD(Report ID)));
CaptionML=ENU=Report Name }
{ 5 ; ;Date Time ;DateTime }
{ ;No. ;Clustered=Yes }

2. Now with the table created for our Report Usage log please open Codeunit 1

3. Open “C/AL Globals”

4. Navigate to ”Functions”


5. Create new function with Name=”OnReportRun”


6. Open Properties and change ID to 120


7. Now open “Locals” and create parameter= ReportId with Type=Integer


8. Select Variables tab and create ReportLog


9. Now all we need is to write the code for this new trigger. Open C/AL Editor and navigate to the end.

10. In OnReportRun write the following code:

ReportLog."User ID" := USERID;

ReportLog."Report ID" := ReportId;

ReportLog."Date Time" := CURRENTDATETIME;



11. Now Restart Classic Client

12. Run a couple of reports

13. And then at last run the Report Log table to see the result:


Yes I’m aware that this solution only works for Classic Reports, and yes I also would like a feature so it’s possible for you to log all objects being used at a customer site. For now we do not get this, but let’s see what the future brings.

/Claus Lundstrøm

Comments (31)

  1. Natalie K. says:

    Thanks Claus! 🙂

    But do we really need a new codeunit 1 function for that? Couldn't we use FindPrinter instead, at least for all reports that are not ProcessingOnly?

  2. clausl says:

    FindPrinter is not visited if UseSystemPrinter=Yes

    /Claus Lundstrøm

  3. Kine says:

    Claus, you have new big plus on my list… 😀

  4. Hannes Holst says:

    a nice solution, of course.

    but what about "a another user blocked the Table. please while the user ends is work"-Problem on a SQL-Server?!

    i'm using a similar solution to log about 1000 – 2000 printed documents a day. and i got this "blocked table"-Problem in average 1 or 2 times a day. sometimes up to 10 times. depending on the operating behavior of the user.

    do you have any practical insights with your solution?

    and by the way, what's the mystery about the ID 120 ?! Are there more like these and for what?!

    …. certainly 😉



  5. Hannes Holst says:

    complete: "please wait while the user ends his work"

    where is my coffee..??? 😉

  6. thad@edgerunner.org says:

    First let me say, THANK YOU!!!!!  I've been wishing for this the last 4 years.  That said, does this trigger occur outside of the normal transaction?  In other words, if a report is run that errors out, will the rollback affect the tracking entry as well?  If so, that would greatly diminish the usefulness of this trigger.

  7. thad@edgerunner.org says:

    Also, if it does occur outside of the transaction that the report runs within, then I believe any locking concerns can be dismissed since any change from the OnReportRun procedure would be committed rather quickly (assuming you don't do anything time intensive within the procedure).  Although I'm pretty sure Claus's example code would have no locking issues anyway since it simply inserts new records and it is letting SQL handle the auto-incrementing.

  8. clausl says:

    OnReportRun is called before the first trigger of the report. So it will commit its inserts no matter what happens inside report run (of course if you are running a report inside an outer transaction – then everything will be committed only when the outer transaction commits.)

    /Claus Lundstrøm

  9. Wøhliche says:

    Dynamics NAV 2009: KB2556639 should be released according to the article above from the NAV Team, but it seems to be found nowhere – It should be released on the following site, Overview of Released Application Hotfixes for Microsoft Dynamics NAV 2009 SP1 and Microsoft Dynamics NAV 2009 R2



    I believe a mistake from the NAV Team, so waiting a couple of days hopefully helps 🙂

  10. clausl says:

    The KB article itself will take some time to produce. While we wait for that you can just request KB2556639 from support.

    For me it was important that you could actually get the hotfix. Should I have waited to blog about this before the KB article was ready as well?

    /Claus Lundstrøm

  11. Bernd says:

    "but what about a another user blocked the Table"

    To prevent this you can add another field with "GUID" to the log table. Change the clustered index to this GUID field. So if another record will be inserted this is always done in another (random) place. So the risk of blocking decreases.

  12. clausl says:

    Updated blog with link to the KB article for NAV 2009.

    /Claus Lundstrøm

  13. Dear Claus,

    at first: Tank you for this great post. Very good that you get this done!

    One suggestion: You created within your Report Log table a Table Relation to User Role by User ID, wich makes no sense. User Role table is used for the permission system. I think you want to link the Field to table ID 91 "User Setup" instead.

    Anyways, damn good implementation.

    @Hannes Holst: The "mystery" about the method ID 120 is: The .exe tries to find the method for report usage logging by id. If you hack-down the exe by reading the executable with a hex-editor, you'll get it more clearly 😉

    I start now with some more test. If I'll finde some more interesting topics, you'll hear it here 🙂

    digital greetingx


  14. Mike G says:

    This seems to work great for reports that are not run modally, however any REPORT.RUNMODAL causes the "the following C/AL functions can be used only to a limited degree during write transactions because one or more tables will be locked." error. An example is trying to print form the posted purchase receipt or posted sales invoice form.

    Anything wrong with putting a COMMIT after the insert to the Report Log? It solves the error.

  15. Thaddeus R says:

    I do see one potential problem with this Mike.  If a report is being run as part of some larger process, then when you COMMIT after the insert you would also commit any writes up to that point from the surrounding larger process.  I personally don't think this is worth the risk, since the larger enclosing process (or the report) could encounter an error and then you would be leaving partially committed data.

  16. Thomas K. says:

    Any new information/experience with this feature, performance-wise?

  17. Jon K says:

    In regards to the RUNMODAL issue and avoiding a commit, an easy work around I am using is to create a table of "exceptions" and put

    if not get.exceptiontable(reportid) then begin


    around Claus' code in codeunit 1 and as users come up with the RUNMODAL error, populate the exceptions table with the Report IDs.  Not a perfect solution but still gets you a list of reports users are using by merging the two tables.

  18. Rolf Kaufmann says:

    Hi Claus,

    I have the same problems as others with this code.

    Report.RUNMODAL causes a Transaction Error.

    and I found out, that your Statement:

     >>OnReportRun is called before the first trigger of the report.

    is not correct. I stepped though the code with the debugger and fuction 120 in CU1 is executed AFTER the Report.OnInit Trigger.

  19. Timo Lässer [MSDynamics.de] says:

    I also got the RUNMODAL error and solved this with a SingleInstance Codeunit:

    The OnReportRun in Codeunit 1 calls a function of my SI Codeunit with the ReportID as parameter which saves all needed informations in a temporary table (defined as global Var in the SI Codeunit).

    In Codeunit 1 – LoginEnd, I call another function of my SI Codeunit, which writes the records of the temporary table to the database.

    This way, I can run reports modal and doesn't need a commit.

    Sure, the statistics are updated at the end of the user session and new entries will be lost if the client crashes in any way, but in total, you will get an overview of the report usage anyway.

    If you need exact informations in real time, this workaround is nothing for you. 😉

  20. dtacconi says:

    PART 1

    Many thanks to Timo to show us a way to implement this @best (of course you do not have the buffered report for still-on-line users). But as wisely stated, this should give the big picture related to logged report.

    I have to say that I have touched the RUNMODAL problem with my own fingers and I just would like to add here AS IS and simple Proof Of Concept a way to get rid of RUNMODAL together with gathering the appropriate info.

    From this blog you just have to create / change the following (again thanks Timo to illustrate a good investigation path):

  21. dtacconi says:


    1. Create a brand new SINGLE INSTANCE codeunit (called e.g. “Report Log Management”) like the following

    these are the GLOBALS

    Name    DataType             Subtype               Length

    tempReportLog                Record  Report Log          

    ReportLog           Record  Report Log          

    counter                Integer                

    entryNo               Integer                

    these are the FUNCTIONS

    WriteTempReportLogUsage(ReportId : Integer)

    IF tempReportLog.FINDLAST THEN

     counter := tempReportLog."No." + 1


     counter := 0;

    tempReportLog."No." := counter;

    tempReportLog."User ID" := USERID;

    tempReportLog."Report ID" := ReportId;

    tempReportLog."Date Time" := CURRENTDATETIME;





     ReportLog."No." := 0;

     ReportLog."User ID" := tempReportLog."User ID";

     ReportLog."Report ID" := tempReportLog."Report ID";

     ReportLog."Date Time" := tempReportLog."Date Time";


    UNTIL tempReportLog.NEXT = 0;

    2. Then you can change your code in Codeunit 1 to populate Temp Table runtime with the following (just refined without logging NAS activity, if any)

    OnReportRun(ReportId : Integer)



  22. DTACCONI says:


    3. And Commit the temp table records into the Report Log table in Codeunit 1 Function LoginEnd


    IF LogInWorkDate <> 0D THEN

     IF LogInWorkDate = LogInDate THEN



       WORKDATE := LogInWorkDate;



       UserSetupFound := TRUE;

       RegisterTime := UserSetup."Register Time";


     IF NOT UserSetupFound THEN

       IF GetGLSetup THEN

         RegisterTime := GLSetup."Register Time";

     IF RegisterTime THEN BEGIN

       LogOutDate := TODAY;

       LogOutTime := TIME;

       IF (LogOutDate > LogInDate) OR (LogOutDate = LogInDate) AND (LogOutTime > LogInTime) THEN

         Minutes := ROUND((1440 * (LogOutDate – LogInDate)) + ((LogOutTime – LogInTime) / 60000),1);

       IF Minutes = 0 THEN

         Minutes := 1;


       UserTimeRegister."User ID" := USERID;

       UserTimeRegister.Date := LogInDate;

       IF UserTimeRegister.FIND THEN BEGIN

         UserTimeRegister.Minutes := UserTimeRegister.Minutes + Minutes;



         UserTimeRegister.Minutes := Minutes;





    ReportLogManagement.CommitReportLogUsage; //ADD THIS LINE

    Hope this helps more in deploy the content of this blog without any side issue.

  23. Benjamin Hochstetter says:

    Thank you for the interesting post. Is it planned to implement the trigger also for RTC. We are planing to upgrade our NAV 2009 RTC Customers to NAV 2013.

  24. twm@twm.dk says:


    We do not have any run always triggers for reports, but what you can do is to make the same logging code inside the Codeunit1::FindPrinter trigger. C/AL SaveAsX methods invoked from the server will not invoke the trigger.

  25. Appstar Financial says:

    Great post! Thanks for sharing this! Very informative. I’m sure this is very helpful for other people too who are into Payment processing programs.

  26. Ann says:


    how does this work in NAV 2013 and NAV 2013 R2?

  27. Mar says:

    To avoid the RUNMODAL error

    Just a thought, but can we use a linked table that is outside transaction scope?

    ie. LinkedInTransaction is set yo false in a linked table?

  28. Vlad Sin says:

    ADOConnection can be used to log changes without having RUNMODAL issue. And since my customer prints thousands of labels and reports daily, we agreed that we will not log every report execution(performance killer), but just collect report IDs that are in use. So we ended up with one-field table and the following code in CU1:

    PROCEDURE OnReportRun@120(ReportID@1060000 : Integer);
    ReportUsage@1060004 : Record 80000;
    ADOConnection@1060003 : Automation “{2A75196C-D9EB-4129-B803-931327F72D5C} 2.8:{00000514-0000-0010-8000-00AA006D2EA4}:’Microsoft ActiveX Data Objects 2.8 Library’.Connection”;
    ADORecordSet@1060002 : Automation “{2A75196C-D9EB-4129-B803-931327F72D5C} 2.8:{00000556-0000-0010-8000-00AA006D2EA4}:’Microsoft ActiveX Data Objects 2.8 Library’._Recordset”;
    ResText@1060001 : Text[250];
    IF NOT ReportUsage.GET(ReportID) THEN BEGIN
    IF ISCLEAR(ADOConnection) THEN
    ADOConnection.CommandTimeout := 30;
    ADOConnection.ConnectionTimeout := 120;
    ADOConnection.ConnectionString := STRSUBSTNO(‘Provider=SQLOLEDB;Data Source=%1;Initial Catalog=%2;User Id=%3;Password=%4;’,
    ADORecordSet := ADOConnection.Execute(
    ‘IF NOT EXISTS (SELECT TOP 1 NULL FROM [dbo].[TEST$Report Usage] ‘ +
    ‘WHERE [Rep ID] = ‘ + FORMAT(ReportID) + ‘) ‘ +
    ‘INSERT INTO [dbo].[TEST$Report Usage] ([Rep ID]) VALUES (‘ + FORMAT(ReportID) + ‘)’,

  29. vsuresh says:

    i tryed above pdf but not storing in tables

  30. supakit says:

    hi i using navision 2009 R2 ,Where to download?

Skip to main content