Difference between revisions of "TimeWorkedReport"

From Request Tracker Wiki
Jump to navigation Jump to search
 
(3 intermediate revisions by 3 users not shown)
Line 1: Line 1:
'''SUMMARY'''
## SUMMARY
 
''Update''
 
''This contribution has now been properly packaged for RT as an extension. It has been updated and configured for RT 4. Refer to the link in See Also.''


This report allows the user to specify a datetime range and one or more queues, and displays the time worked for each ticket in the selected queue(s) which has > 0 minutes time worked in the datetime range. It will show non-superusers their own report and superusers a report of all users.
This report allows the user to specify a datetime range and one or more queues, and displays the time worked for each ticket in the selected queue(s) which has > 0 minutes time worked in the datetime range. It will show non-superusers their own report and superusers a report of all users.
Line 7: Line 11:
Written by Fran Fabrizio, fabrizio -at- uab -dot- edu.
Written by Fran Fabrizio, fabrizio -at- uab -dot- edu.


'''REQUIREMENTS'''
## REQUIREMENTS


This is known to work with RT 3.8.5. The changes needed to make it work with RT 4.0.x are outlined below.
This is known to work with RT 3.8.5. The changes needed to make it work with RT 4.0.x are outlined below.


'''KNOWN BUGS'''
## KNOWN BUGS


At least two users have reported a problem with RT 3.6.x and errors related to undefined principal within the [[HasRight]] sub of [[Queue Overlay|Queue_Overlay]].pm. The RT developers changed [[HasRight]] between 3.6 and 3.8 and the code changes seem centered around undefined principals, so if anyone knows what this is and how I can code around it in a way that would work for 3.6, please contact me.
At least two users have reported a problem with RT 3.6.x and errors related to undefined principal within the [[HasRight]] sub of [[Queue Overlay|Queue_Overlay]].pm. The RT developers changed [[HasRight]] between 3.6 and 3.8 and the code changes seem centered around undefined principals, so if anyone knows what this is and how I can code around it in a way that would work for 3.6, please contact me.


'''TODO'''
## SEE ALSO
 
http://search.cpan.org/dist/RT-Extension-ActivityReports/
 
''Updated version below:''
 
https://metacpan.org/pod/RT::Extension::TimeWorkedReport
 
If you upgrade to the new version and had the old one previously instaled, you will need to remove the files you created under ../local both for html and callbacks, and then clear the mason object cache in order for the new one to start working.
 
TODO


This extension is still in active development. Currently, the TODO list is:
This extension is still in active development. Currently, the TODO list is:
Line 25: Line 39:
* Allow users to control extension behavior via RT's Configuration area
* Allow users to control extension behavior via RT's Configuration area


'''FEEDBACK'''
## FEEDBACK


If you have other suggestions/requests, please email me at fabrizio -at- uab -dot- edu. This is still early in development; expect rough edges.
If you have other suggestions/requests, please email me at fabrizio -at- uab -dot- edu. This is still early in development; expect rough edges.


'''INSTALLATION'''
## INSTALLATION


1. Copy $RT_HOME/share/html/Tools/Reports/index.html to $RT_HOME/local/html/Tools/Reports/index.html
1. Copy $RT_HOME/share/html/Tools/Reports/index.html to $RT_HOME/local/html/Tools/Reports/index.html
Line 35: Line 49:
Edit $RT_HOME/local/html/Tools/Reports/index.html and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 56 of the file):
Edit $RT_HOME/local/html/Tools/Reports/index.html and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 56 of the file):


  D => {
<syntaxhighlight lang="perl" line="1" >
     title      =&gt; loc('Time Worked Report'),
  D => {
     path        =&gt; '/Tools/Reports/TimeWorkedReport.html',
     title      => loc('Time Worked Report'),
     description =&gt; loc('A Time Worked Report'),
     path        => '/Tools/Reports/TimeWorkedReport.html',
     description => loc('A Time Worked Report'),
  },
  },
</syntaxhighlight>


You may need to change the "D" to the next available letter, depending on other mods that you may have made.
You may need to change the "D" to the next available letter, depending on other mods that you may have made.
Line 48: Line 63:
Edit $RT_HOME/local/html/Tools/Reports/Elements/Tabs and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 55 of that file):
Edit $RT_HOME/local/html/Tools/Reports/Elements/Tabs and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 55 of that file):


<syntaxhighlight lang="perl" line="1" >
  d =&gt; {
  d =&gt; {
     title =&gt; loc('Time Worked Report'),
     title =&gt; loc('Time Worked Report'),
     path  =&gt; 'Tools/Reports/TimeWorkedReport.html',
     path  =&gt; 'Tools/Reports/TimeWorkedReport.html',
  },
  },
</syntaxhighlight>


Again, you may need to change the "d" to the next available letter depending on other mods you may have made.
Again, you may need to change the "d" to the next available letter depending on other mods you may have made.
Line 58: Line 74:
3. Create the file $RT_HOME/local/html/Elements/SelectMultiQueue with the following content:
3. Create the file $RT_HOME/local/html/Elements/SelectMultiQueue with the following content:


<nowiki>%# BEGIN BPS TAGGED BLOCK {{{
<syntaxhighlight lang="perl" line="1" >
%#
      %# BEGIN BPS TAGGED BLOCK {{{
%# COPYRIGHT:
      %#
%#
      %# COPYRIGHT:
%# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
      %#
%#                                          &lt;jesse@bestpractical.com&gt;
      %# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
%#
      %#                                          &lt;jesse@bestpractical.com&gt;
%# (Except where explicitly superseded by other copyright notices)
      %#
%#
      %# (Except where explicitly superseded by other copyright notices)
%#
      %#
%# LICENSE:
      %#
%#
      %# LICENSE:
%# This work is made available to you under the terms of Version 2 of
      %#
%# the GNU General Public License. A copy of that license should have
      %# This work is made available to you under the terms of Version 2 of
%# been provided with this software, but in any event can be snarfed
      %# the GNU General Public License. A copy of that license should have
%# from www.gnu.org.
      %# been provided with this software, but in any event can be snarfed
%#
      %# from www.gnu.org.
%# This work is distributed in the hope that it will be useful, but
      %#
%# WITHOUT ANY WARRANTY; without even the implied warranty of
      %# This work is distributed in the hope that it will be useful, but
%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
      %# WITHOUT ANY WARRANTY; without even the implied warranty of
%# General Public License for more details.
      %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
%#
      %# General Public License for more details.
%# You should have received a copy of the GNU General Public License
      %#
%# along with this program; if not, write to the Free Software
      %# You should have received a copy of the GNU General Public License
%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
      %# along with this program; if not, write to the Free Software
%# 02110-1301 or visit their web page on the internet at
      %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
      %# 02110-1301 or visit their web page on the internet at
%#
      %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
%#
      %#
%# CONTRIBUTION SUBMISSION POLICY:
      %#
%#
      %# CONTRIBUTION SUBMISSION POLICY:
%# (The following paragraph is not intended to limit the rights granted
      %#
%# to you to modify and distribute this software under the terms of
      %# (The following paragraph is not intended to limit the rights granted
%# the GNU General Public License and is only of importance to you if
      %# to you to modify and distribute this software under the terms of
%# you choose to contribute your changes and enhancements to the
      %# the GNU General Public License and is only of importance to you if
%# community by submitting them to Best Practical Solutions, LLC.)
      %# you choose to contribute your changes and enhancements to the
%#
      %# community by submitting them to Best Practical Solutions, LLC.)
%# By intentionally submitting any modifications, corrections or
      %#
%# derivatives to this work, or any other work intended for use with
      %# By intentionally submitting any modifications, corrections or
%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
      %# derivatives to this work, or any other work intended for use with
%# you are the copyright holder for those contributions and you grant
      %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
      %# you are the copyright holder for those contributions and you grant
%# royalty-free, perpetual, license to use, copy, create derivative
      %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
%# works based on those contributions, and sublicense and distribute
      %# royalty-free, perpetual, license to use, copy, create derivative
%# those contributions and any derivatives thereof.
      %# works based on those contributions, and sublicense and distribute
%#
      %# those contributions and any derivatives thereof.
%# END BPS TAGGED BLOCK }}}
      %#
% if ($Lite) {
      %# END BPS TAGGED BLOCK }}}
%    my $d = new RT::Queue($session{'CurrentUser'});
      % if ($Lite) {
%    $d-&gt;Load($Default);
      %    my $d = new RT::Queue($session{'CurrentUser'});
&lt;input name="&lt;%$Name%&gt;" size="25" value="&lt;%$d-&gt;Name%&gt;" class="&lt;%$Class%&gt;" /&gt;
      %    $d-&gt;Load($Default);
% }
      &lt;input name="&lt;%$Name%&gt;" size="25" value="&lt;%$d-&gt;Name%&gt;" class="&lt;%$Class%&gt;" /&gt;
% else {
      % }
%    # $Default will be an arrayref if multiple queues are selected, or a
      % else {
%    # scalar if 0-1 queues are selected.  Hence, this ugly processing logic.
      %    # $Default will be an arrayref if multiple queues are selected, or a
%    my %selected;
      %    # scalar if 0-1 queues are selected.  Hence, this ugly processing logic.
%    if (ref $Default) {
      %    my %selected;
%      for (@$Default) {
      %    if (ref $Default) {
%        $selected{$_} = 1;
      %      for (@$Default) {
%      }
      %        $selected{$_} = 1;
%    } else {
      %      }
%      $selected{$Default} = 1;
      %    } else {
%    }
      %      $selected{$Default} = 1;
&lt;select name="&lt;%$Name%&gt;" &lt;% ($OnChange) ? 'onchange="'.$OnChange.'"' : '' |n %&gt; class="  &lt;%$Class%&gt;" MULTIPLE&gt;
      %    }
%    if ($ShowNullOption) {
      &lt;select name="&lt;%$Name%&gt;" &lt;% ($OnChange) ? 'onchange="'.$OnChange.'"' : '' |n %&gt; class="  &lt;%$Class%&gt;" MULTIPLE&gt;
  &lt;option value=""&gt;-&lt;/option&gt;
      %    if ($ShowNullOption) {
%    }
        &lt;option value=""&gt;-&lt;/option&gt;
%    for my $queue (@{$session{$cache_key}}) {
      %    }
  &lt;option value="&lt;% ($NamedValues ? $queue-&gt;{Name} : $queue-&gt;{Id}) %&gt;"
      %    for my $queue (@{$session{$cache_key}}) {
        &lt;option value="&lt;% ($NamedValues ? $queue-&gt;{Name} : $queue-&gt;{Id}) %&gt;"
%# if ($queue-&gt;{Id} eq ($Default||'') || $queue-&gt;{Name} eq ($Default||'')) {
     
% if($selected{$queue-&gt;{Id}}) {
      %# if ($queue-&gt;{Id} eq ($Default||'') || $queue-&gt;{Name} eq ($Default||'')) {
  selected="selected"
      % if($selected{$queue-&gt;{Id}}) {
% }
      selected="selected"
      % }
&gt;
     
    &lt;%$queue-&gt;{Name}%&gt;
      &gt;
          &lt;%$queue-&gt;{Name}%&gt;
%            if ($Verbose and $queue-&gt;{Description}) {
     
    (&lt;%$queue-&gt;{Description}%&gt;)
      %            if ($Verbose and $queue-&gt;{Description}) {
%            }
          (&lt;%$queue-&gt;{Description}%&gt;)
  &lt;/option&gt;
      %            }
%    }
        &lt;/option&gt;
&lt;/select&gt;
      %    }
% }
      &lt;/select&gt;
&lt;%args&gt;
      % }
$CheckQueueRight =&gt; 'CreateTicket'
      &lt;%args&gt;
$ShowNullOption =&gt; 1
      $CheckQueueRight =&gt; 'CreateTicket'
$ShowAllQueues =&gt; 1
      $ShowNullOption =&gt; 1
$Name =&gt; undef
      $ShowAllQueues =&gt; 1
$Verbose =&gt; undef
      $Name =&gt; undef
$NamedValues =&gt; 0
      $Verbose =&gt; undef
$Default =&gt; 0
      $NamedValues =&gt; 0
$Lite =&gt; 0
      $Default =&gt; 0
$OnChange =&gt; undef
      $Lite =&gt; 0
$Class =&gt; 'select-queue'
      $OnChange =&gt; undef
&lt;/%args&gt;
      $Class =&gt; 'select-queue'
&lt;%init&gt;
      &lt;/%args&gt;
my $cache_key = "SelectQueue---"
      &lt;%init&gt;
                . $session{'CurrentUser'}-&gt;Id
      my $cache_key = "SelectQueue---"
                . "---$CheckQueueRight---$ShowAllQueues";
                      . $session{'CurrentUser'}-&gt;Id
                      . "---$CheckQueueRight---$ShowAllQueues";
if (not defined $session{$cache_key} and not $Lite) {
     
    my $q = new RT::Queues($session{'CurrentUser'});
      if (not defined $session{$cache_key} and not $Lite) {
    $q-&gt;UnLimit;
          my $q = new RT::Queues($session{'CurrentUser'});
          $q-&gt;UnLimit;
    while (my $queue = $q-&gt;Next) {
     
        if ($ShowAllQueues || $queue-&gt;CurrentUserHasRight($CheckQueueRight)) {
          while (my $queue = $q-&gt;Next) {
            push @{$session{$cache_key}}, {
              if ($ShowAllQueues || $queue-&gt;CurrentUserHasRight($CheckQueueRight)) {
                Id          =&gt; $queue-&gt;Id,
                  push @{$session{$cache_key}}, {
                Name        =&gt; $queue-&gt;Name,
                      Id          =&gt; $queue-&gt;Id,
                Description =&gt; $queue-&gt;Description,
                      Name        =&gt; $queue-&gt;Name,
            };
                      Description =&gt; $queue-&gt;Description,
        }
                  };
    }
              }
}
          }
&lt;/%init&gt;
      }
      &lt;/%init&gt;
</nowiki>
     
</syntaxhighlight>


This is a very slightly modified form of SelectQueue which ships with RT in the same directory. You can diff the two to see what I have changed.
This is a very slightly modified form of SelectQueue which ships with RT in the same directory. You can diff the two to see what I have changed.
Line 182: Line 199:
4. Create the file $RT_HOME/local/html/Tools/Reports/TimeWorkedReport.html with the following content:
4. Create the file $RT_HOME/local/html/Tools/Reports/TimeWorkedReport.html with the following content:


<nowiki>&lt;%args&gt;
<syntaxhighlight lang="perl" line="1" >
  $startdate =&gt; undef
      &lt;%args&gt;
  $enddate  =&gt; undef
      $startdate =&gt; undef
  $queues    =&gt; undef
      $enddate  =&gt; undef
  $byticket  =&gt; undef
      $queues    =&gt; undef
&lt;/%args&gt;
      $byticket  =&gt; undef
      &lt;/%args&gt;
&lt;&amp; /Elements/Header, Title =&gt; $title &amp;&gt;
     
&lt;&amp; /Tools/Reports/Elements/Tabs, current_tab =&gt; 'Tools/Reports/TimeWorkedReport.html', Title  =&gt; $title &amp;&gt;
      &lt;&amp; /Elements/Header, Title =&gt; $title &amp;&gt;
&lt;hr&gt;
      &lt;&amp; /Tools/Reports/Elements/Tabs, current_tab =&gt; 'Tools/Reports/TimeWorkedReport.html', Title  =&gt; $title &amp;&gt;
      &lt;hr&gt;
&lt;%init&gt;
     
my ($start_date, $end_date, $effective_end_date, $title);
      &lt;%init&gt;
      my ($start_date, $end_date, $effective_end_date, $title);
$title = loc('Time worked report');
     
      $title = loc('Time worked report');
$start_date = RT::Date-&gt;new($session{'CurrentUser'});
     
$end_date  = RT::Date-&gt;new($session{'CurrentUser'});
      $start_date = RT::Date-&gt;new($session{'CurrentUser'});
      $end_date  = RT::Date-&gt;new($session{'CurrentUser'});
# If we have a value for start date, parse it into an RT::Date object
     
if ($startdate) {
      # If we have a value for start date, parse it into an RT::Date object
  $start_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $startdate);
      if ($startdate) {
  # And then get it back as an ISO string for display purposes, in the form field and
        $start_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $startdate);
  # report header
        # And then get it back as an ISO string for display purposes, in the form field and
  $startdate = $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
        # report header
}
        $startdate = $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
      }
# Same treatment for end date
     
if ($enddate) {
      # Same treatment for end date
  $end_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $enddate);
      if ($enddate) {
  $enddate = $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
        $end_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $enddate);
}
        $enddate = $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
      }
&lt;/%init&gt;
     
      &lt;/%init&gt;
&lt;form method="post" action="TimeWorkedReport.html"&gt;
     
  &lt;br /&gt;
      &lt;form method="post" action="TimeWorkedReport.html"&gt;
  &lt;&amp;|/l&amp;&gt;Start date&lt;/&amp;&gt;:
      &lt;br /&gt;
  &lt;&amp; /Elements/SelectDate, Name =&gt; 'startdate', Default =&gt; ($startdate) ?  $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
        &lt;&amp;|/l&amp;&gt;Start date&lt;/&amp;&gt;:
  (report will start from midnight on this day unless you indicate otherwise)
        &lt;&amp; /Elements/SelectDate, Name =&gt; 'startdate', Default =&gt; ($startdate) ?  $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
  &lt;br /&gt;
        (report will start from midnight on this day unless you indicate otherwise)
  &lt;&amp;|/l&amp;&gt;End date&lt;/&amp;&gt;:
      &lt;br /&gt;
  &lt;&amp; /Elements/SelectDate, Name =&gt; 'enddate', Default =&gt; ($enddate) ?  $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
        &lt;&amp;|/l&amp;&gt;End date&lt;/&amp;&gt;:
  (report will -not- be inclusive of this day unless you change the time from midnight)
        &lt;&amp; /Elements/SelectDate, Name =&gt; 'enddate', Default =&gt; ($enddate) ?  $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
  &lt;br /&gt;
        (report will -not- be inclusive of this day unless you change the time from midnight)
  &lt;&amp;|/l&amp;&gt;Queues&lt;/&amp;&gt;:
      &lt;br /&gt;
  &lt;&amp; /Elements/SelectMultiQueue, Name =&gt; 'queues', Default =&gt; ($queues) ? $queues : ''&amp;&gt;
        &lt;&amp;|/l&amp;&gt;Queues&lt;/&amp;&gt;:
  &lt;br /&gt;
        &lt;&amp; /Elements/SelectMultiQueue, Name =&gt; 'queues', Default =&gt; ($queues) ? $queues : ''&amp;&gt;
  &lt;&amp; /Elements/Checkbox, Name =&gt; 'byticket', Default =&gt; ($byticket) ? 'checked' : ''&amp;&gt;
      &lt;br /&gt;
  Organize report by ticket instead of by person
        &lt;&amp; /Elements/Checkbox, Name =&gt; 'byticket', Default =&gt; ($byticket) ? 'checked' : ''&amp;&gt;
&lt;&amp; /Elements/Submit&amp;&gt;
        Organize report by ticket instead of by person
      &lt;&amp; /Elements/Submit&amp;&gt;
&lt;/form&gt;
     
      &lt;/form&gt;
     
&lt;%perl&gt;
     
# TimeWorkedReport
      &lt;%perl&gt;
# Version 0.04  2009-09-28
      # TimeWorkedReport
#
      # Version 0.04  2009-09-28
# Fran Fabrizio, UAB CIS, fran@cis.uab.edu
      #
      # Fran Fabrizio, UAB CIS, fran@cis.uab.edu
use strict;
     
      use strict;
# if we are just getting here and the form values are empty, we are done
     
if (!$startdate || !$enddate) {
      # if we are just getting here and the form values are empty, we are done
  return;
      if (!$startdate || !$enddate) {
}
        return;
      }
# get the queue object(s)
     
my $queuesobj = new RT::Queues($session{CurrentUser});
      # get the queue object(s)
my ($queuelist, %queuesofinterest);
      my $queuesobj = new RT::Queues($session{CurrentUser});
      my ($queuelist, %queuesofinterest);
# The user's choice of queues will come in from the web form in the $queues variable, which is
     
# mapped to the SELECT field on the web interface for the report.  Unfortunately, if the user
      # The user's choice of queues will come in from the web form in the $queues variable, which is
# chooses just one queue, $queues will have a scalar value, but if the user chooses multiple
      # mapped to the SELECT field on the web interface for the report.  Unfortunately, if the user
# queues, it will be an arrayref.  So we need to check for each case and process differently.
      # chooses just one queue, $queues will have a scalar value, but if the user chooses multiple
#
      # queues, it will be an arrayref.  So we need to check for each case and process differently.
# What we want to construct is the %queuesofinterest simple lookup hash which defines a key
      #
# that is the queue ID for each queue selected, and the $queuelist string, which is just for
      # What we want to construct is the %queuesofinterest simple lookup hash which defines a key
# displaying the list of queues in the report header
      # that is the queue ID for each queue selected, and the $queuelist string, which is just for
$queues = [ $queues ] unless ref($queues);
      # displaying the list of queues in the report header
 
      $queues = [ $queues ] unless ref($queues);
for (@$queues) {
   
  $queuesobj->Limit(FIELD => "Id", OPERATOR => "=", VALUE => $_, ENTRYAGGREGATOR => "OR");
      for (@$queues) {
  $queuesofinterest{$_} = 1;
        $queuesobj->Limit(FIELD => "Id", OPERATOR => "=", VALUE => $_, ENTRYAGGREGATOR => "OR");
}
        $queuesofinterest{$_} = 1;
$queuelist = join ", ", map {$_->Name} @{$queuesobj->ItemsArrayRef};
# hash to hold statistics
# %stats will be a multilevel hash - first level keys are the usernames, second level keys are
# the ticket IDs, and for each ticket, we store an anonymous hash with keys Subject and  TimeWorked
# (this implies that a single ticket can live under two+ users if they both worked the ticket)
my %stats;
# Get a new transactions object to hold transaction search results for this ticket
my $trans = new RT::Transactions($session{'CurrentUser'});
# only in the period of interest
$trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&gt;', VALUE =&gt; $startdate);
$trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&lt;', VALUE =&gt; $enddate, ENTRYAGGREGATOR =&gt;  'AND');
# now start counting all the TimeTaken by examining transactions associated with this ticket
while (my $tr = $trans-&gt;Next) {
  # did this transaction take any time?  RT records this -either- in TimeTaken column or by
  # indicating "TimeWorked" in the Field column, depending on how the user inputted the time.
  if (($tr-&gt;TimeTaken != 0) || ($tr-&gt;Field &amp;&amp; $tr-&gt;Field eq 'TimeWorked')) {
    # Got a hot one - what ticket is this?
    my $t = new RT::Ticket($session{'CurrentUser'});
    $t-&gt;Load($tr-&gt;ObjectId);
    if (!$t) {
      # unable to retrieve a ticket for this transaction
      # hopefully we don't ever reach here!
      next;
    } else {
      # Is a queue selected and is this ticket in a queue we care about?
      if ($queuelist && !$queuesofinterest{$t-&gt;Queue}) {
        next;
       }
       }
    }
      $queuelist = join ", ", map {$_->Name} @{$queuesobj->ItemsArrayRef};
   
     
    # If this is time logged by user RT_System, it's the result of a ticket merge
      # hash to hold statistics
    # In order to avoid double-counting minutes in --byticket mode, or the less serious
      # %stats will be a multilevel hash - first level keys are the usernames, second level keys are
    # issue of displaying a report for user RT_System in normal mode, we skip this entirely
      # the ticket IDs, and for each ticket, we store an anonymous hash with keys Subject and  TimeWorked
    if ($tr-&gt;CreatorObj-&gt;Name eq 'RT_System') {
      # (this implies that a single ticket can live under two+ users if they both worked the ticket)
      next;
      my %stats;
    }
     
      # Get a new transactions object to hold transaction search results for this ticket
    # we've got some time to account for
      my $trans = new RT::Transactions($session{'CurrentUser'});
     
    # is this the first time this person is charging time to this ticket?
      # only in the period of interest
    # if so, add this ticket subject to the data structure
      $trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&gt;', VALUE =&gt; $startdate);
    if (!exists($stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject})) {
      $trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&lt;', VALUE =&gt; $enddate, ENTRYAGGREGATOR =&gt; 'AND');
      $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject} = $t-&gt;Subject;
     
    }
      # now start counting all the TimeTaken by examining transactions associated with this ticket
      while (my $tr = $trans-&gt;Next) {
    if ($tr-&gt;TimeTaken != 0) {
     
      # this was a comment or correspondence where the user also added some time worked
      # did this transaction take any time?  RT records this -either- in TimeTaken column or by
      # value of interest appears in Transaction's TimeTaken column
      # indicating "TimeWorked" in the Field column, depending on how the user inputted the time.
      $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;TimeTaken;
      if (($tr-&gt;TimeTaken != 0) || ($tr-&gt;Field &amp;&amp; $tr-&gt;Field eq 'TimeWorked')) {
    } else {
        # Got a hot one - what ticket is this?
      # this was a direct update of the time worked field from the Basics or Jumbo ticket update page
        my $t = new RT::Ticket($session{'CurrentUser'});
      # values of interest appear in Transaction's OldValue and NewValue columns
        $t-&gt;Load($tr-&gt;ObjectId);
      # RT does not use the TimeTaken column in this instance.
     
      $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;NewValue - $tr-&gt;OldValue;
        if (!$t) {
    }
          # unable to retrieve a ticket for this transaction
  }
          # hopefully we don't ever reach here!
}
          next;
        } else {
# report output starts here
          # Is a queue selected and is this ticket in a queue we care about?
# output:
          if ($queuelist && !$queuesofinterest{$t-&gt;Queue}) {
#  normal user: their own time worked report, most worked ticket to least worked ticket
            next;
#  superuser:  everyone's time worked report, in username alpha order, then by most worked to least worked
          }
#  superuser+byticket: most worked ticket first, with everyone's contribution ranked by  biggest contribution to smallest
        }
     
print "&lt;h2&gt;TIME WORKED REPORT FOR QUEUE(S) " . $queuelist . "&lt;/h2&gt;";
        # If this is time logged by user RT_System, it's the result of a ticket merge
print "&lt;h3&gt;Date Range: $startdate TO $enddate&lt;/h3&gt;";
        # In order to avoid double-counting minutes in --byticket mode, or the less serious
if ($byticket) {
        # issue of displaying a report for user RT_System in normal mode, we skip this entirely
  print "&lt;h3&gt;Organized by Ticket&lt;/h3&gt;";
        if ($tr-&gt;CreatorObj-&gt;Name eq 'RT_System') {
}
          next;
print "&lt;hr&gt;";
        }
     
# if this person is not a superuser, we should only show them the report for themselves
        # we've got some time to account for
# which means we should remove all keys from %stats except their own username
     
if (!($session{'CurrentUser'}-&gt;HasRight(Right =&gt; 'SuperUser', Object =&gt; $RT::System))) {
        # is this the first time this person is charging time to this ticket?
  my %tempstats;
        # if so, add this ticket subject to the data structure
  $tempstats{$session{CurrentUser}-&gt;Name} = $stats{$session{CurrentUser}-&gt;Name};
        if (!exists($stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject})) {
  %stats = %tempstats;
          $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject} = $t-&gt;Subject;
}
        }
     
if ($byticket) {
        if ($tr-&gt;TimeTaken != 0) {
  # if we're going to organize this by ticket, we need to transform the data first
          # this was a comment or correspondence where the user also added some time worked
  # HAVE ENTRIES LIKE:  $stats{JoeUser}{12345}{TimeWorked} = 150
          # value of interest appears in Transaction's TimeTaken column
  #                    $stats{JoeUser}{12345}{Subject} = "Fix the Fubar Widget"
          $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;TimeTaken;
  # WANT ENTRIES LIKE:  $tstats{12345}{TotalTime} = 250
        } else {
  #                    $tstats{12345}{Subject} = "Fix the Fubar Widget"
          # this was a direct update of the time worked field from the Basics or Jumbo ticket update page
  #                    $tstats{12345}{People}{JoeUser} = 150
          # values of interest appear in Transaction's OldValue and NewValue columns
  #                    $tstats{12345}{People}{JaneDoe} = 100
          # RT does not use the TimeTaken column in this instance.
          $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;NewValue - $tr-&gt;OldValue;
  my %tstats;
         }
  for my $person (keys %stats) {
    for my $tid (keys %{$stats{$person}}) {
      # grab the subject line if you don't have it already
      if (!exists($tstats{$tid}{Subject})) {
         $tstats{$tid}{Subject} = $stats{$person}{$tid}{Subject};
       }
       }
      # now increment total time for this ticket
      }
      $tstats{$tid}{TotalTime} += $stats{$person}{$tid}{TimeWorked};
     
      # and record this user's contribution to this ticket
      # report output starts here
      $tstats{$tid}{People}{$person} = $stats{$person}{$tid}{TimeWorked};
      # output:
    }
      # normal user: their own time worked report, most worked ticket to least worked ticket
  }
      #  superuser:  everyone's time worked report, in username alpha order, then by most worked to least worked
      #  superuser+byticket: most worked ticket first, with everyone's contribution ranked by  biggest contribution to smallest
  # Now emit the report
     
  for my $tid (sort {$tstats{$b}{TotalTime} &lt;=&gt; $tstats{$a}{TotalTime}} keys %tstats) {
      print "&lt;h2&gt;TIME WORKED REPORT FOR QUEUE(S) " . $queuelist . "&lt;/h2&gt;";
    my $subject = $tstats{$tid}{Subject};
      print "&lt;h3&gt;Date Range: $startdate TO $enddate&lt;/h3&gt;";
    print "&lt;H3&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/H3&gt;";
      if ($byticket) {
    print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
        print "&lt;h3&gt;Organized by Ticket&lt;/h3&gt;";
    printf("&lt;TR&gt;&lt;TH WIDTH=30&gt;&lt;/TH&gt;&lt;TH&gt;%dm&lt;/TH&gt;&lt;TH&gt;%.1fh&lt;/TH&gt;&lt;TH&gt;TOTAL TIME&lt;/TH&gt;&lt;/TR&gt;",  $tstats{$tid}{TotalTime},($tstats{$tid}{TotalTime} / 60));
      }
    for my $person (sort {$tstats{$tid}{People}{$b} &lt;=&gt; $tstats{$tid}{People}{$a}} keys %{$tstats{$tid}{People}}) {
      print "&lt;hr&gt;";
      my $minutes = $tstats{$tid}{People}{$person};
     
      printf("&lt;TR&gt;&lt;TD&gt;&lt;/TD&gt;&lt;TD&gt;%dm&lt;/TD&gt;&lt;TD&gt;%.1fh&lt;/TD&gt;&lt;TD&gt;%s&lt;/TD&gt;&lt;/TR&gt;",$minutes,($minutes /60),$person);
      # if this person is not a superuser, we should only show them the report for themselves
    }
      # which means we should remove all keys from %stats except their own username
    print "&lt;/TABLE&gt;";
      if (!($session{'CurrentUser'}-&gt;HasRight(Right =&gt; 'SuperUser', Object =&gt; $RT::System))) {
  }
        my %tempstats;
} else {
        $tempstats{$session{CurrentUser}-&gt;Name} = $stats{$session{CurrentUser}-&gt;Name};
  # the existing %stats data structure is perfect for the default report, no data transform  needed
        %stats = %tempstats;
  for my $person (sort keys %stats) {
      }
    # get the person object, so we can get the FriendlyName to use as header
     
    my $personobj = new RT::User($session{CurrentUser});
      if ($byticket) {
    $personobj-&gt;Load($person);
        # if we're going to organize this by ticket, we need to transform the data first
        # HAVE ENTRIES LIKE:  $stats{JoeUser}{12345}{TimeWorked} = 150
    print "&lt;h3&gt;" . $personobj-&gt;FriendlyName . "&lt;/h3&gt;";
        #                    $stats{JoeUser}{12345}{Subject} = "Fix the Fubar Widget"
    print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
        # WANT ENTRIES LIKE:  $tstats{12345}{TotalTime} = 250
    print "&lt;TR&gt;&lt;TH&gt;MINUTES&lt;/TH&gt;&lt;TH&gt;HOURS&lt;/TH&gt;&lt;TH&gt;TICKET&lt;/TH&gt;&lt;/TR&gt;";
        #                    $tstats{12345}{Subject} = "Fix the Fubar Widget"
    my $totalMinutes = 0;
        #                    $tstats{12345}{People}{JoeUser} = 150
    for my $tid (sort {$stats{$person}{$b}{TimeWorked} &lt;=&gt; $stats{$person}{$a}{TimeWorked}}  keys %{$stats{$person}}) {
        #                    $tstats{12345}{People}{JaneDoe} = 100
      my $minutes = $stats{$person}{$tid}{TimeWorked};
     
      my $subject = $stats{$person}{$tid}{Subject};
        my %tstats;
      print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;${minutes}m&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;" . sprintf("%.1fh",($minutes/60)) . "&lt;/TD&gt;" .
        for my $person (keys %stats) {
                "&lt;TD&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/TD&gt;&lt;/TR&gt;";
          for my $tid (keys %{$stats{$person}}) {
      $totalMinutes += $minutes;
            # grab the subject line if you don't have it already
    }
            if (!exists($tstats{$tid}{Subject})) {
    print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;${totalMinutes}m&lt;/B&gt;&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;" . sprintf("%.1fh",($totalMinutes/60)) . "&lt;/B&gt;&lt;/TD&gt;&lt;TD&gt;&lt;B&gt;TOTALS&lt;/B&gt;&lt;/TD&gt;&lt;/TR&gt;";
              $tstats{$tid}{Subject} = $stats{$person}{$tid}{Subject};
    print "&lt;/TABLE&gt;";
            }
  }
            # now increment total time for this ticket
}
            $tstats{$tid}{TotalTime} += $stats{$person}{$tid}{TimeWorked};
            # and record this user's contribution to this ticket
##### helper functions below
            $tstats{$tid}{People}{$person} = $stats{$person}{$tid}{TimeWorked};
          }
sub form_date_string {
        }
  # expects seven input params - year, month, day, hour, minute, second, offset
     
  my $year = $_[0] - 1900;
        # Now emit the report
  my $mon = $_[1] - 1;
        for my $tid (sort {$tstats{$b}{TotalTime} &lt;=&gt; $tstats{$a}{TotalTime}} keys %tstats) {
  my $day = $_[2];
          my $subject = $tstats{$tid}{Subject};
  my $hour = $_[3] ? $_[3] : 0;
          print "&lt;H3&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/H3&gt;";
  my $min = $_[4] ? $_[4] : 0;
          print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
  my $sec = $_[5] ? $_[5] : 0;
          printf("&lt;TR&gt;&lt;TH WIDTH=30&gt;&lt;/TH&gt;&lt;TH&gt;%dm&lt;/TH&gt;&lt;TH&gt;%.1fh&lt;/TH&gt;&lt;TH&gt;TOTAL TIME&lt;/TH&gt;&lt;/TR&gt;",  $tstats{$tid}{TotalTime},($tstats{$tid}{TotalTime} / 60));
  my $offset = $_[6] ? $_[6] : 0;
          for my $person (sort {$tstats{$tid}{People}{$b} &lt;=&gt; $tstats{$tid}{People}{$a}} keys %{$tstats{$tid}{People}}) {
            my $minutes = $tstats{$tid}{People}{$person};
  # convert to seconds since epoch, then adjust for the $offset, which is also in seconds
            printf("&lt;TR&gt;&lt;TD&gt;&lt;/TD&gt;&lt;TD&gt;%dm&lt;/TD&gt;&lt;TD&gt;%.1fh&lt;/TD&gt;&lt;TD&gt;%s&lt;/TD&gt;&lt;/TR&gt;",$minutes,($minutes /60),$person);
  # we do this so we don't have to do fancy date arithmetic - we can just subtract one seconds
          }
  # value from the other seconds value
          print "&lt;/TABLE&gt;";
  my $starttime = timelocal($sec,$min,$hour,$day,$mon,$year) - $offset;
        }
      } else {
  # convert back to component parts now that we've adjusted for offset
        # the existing %stats data structure is perfect for the default report, no data transform  needed
  # this gives us the components which represent the GMT time for the local time that was entered
        for my $person (sort keys %stats) {
  # on the command line
          # get the person object, so we can get the FriendlyName to use as header
  ($sec,$min,$hour,$day,$mon,$year) = localtime($starttime);
          my $personobj = new RT::User($session{CurrentUser});
          $personobj-&gt;Load($person);
  # format the date string, padding with zeros if needed
     
  return sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $day, $hour, $min, $sec);
          print "&lt;h3&gt;" . $personobj-&gt;FriendlyName . "&lt;/h3&gt;";
}
          print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
          print "&lt;TR&gt;&lt;TH&gt;MINUTES&lt;/TH&gt;&lt;TH&gt;HOURS&lt;/TH&gt;&lt;TH&gt;TICKET&lt;/TH&gt;&lt;/TR&gt;";
&lt;/%perl&gt;
          my $totalMinutes = 0;
          for my $tid (sort {$stats{$person}{$b}{TimeWorked} &lt;=&gt; $stats{$person}{$a}{TimeWorked}}  keys %{$stats{$person}}) {
</nowiki>
            my $minutes = $stats{$person}{$tid}{TimeWorked};
            my $subject = $stats{$person}{$tid}{Subject};
            print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;${minutes}m&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;" . sprintf("%.1fh",($minutes/60)) . "&lt;/TD&gt;" .
                      "&lt;TD&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/TD&gt;&lt;/TR&gt;";
            $totalMinutes += $minutes;
          }
          print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;${totalMinutes}m&lt;/B&gt;&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;" . sprintf("%.1fh",($totalMinutes/60)) . "&lt;/B&gt;&lt;/TD&gt;&lt;TD&gt;&lt;B&gt;TOTALS&lt;/B&gt;&lt;/TD&gt;&lt;/TR&gt;";
          print "&lt;/TABLE&gt;";
        }
      }
     
      ##### helper functions below
     
      sub form_date_string {
      # expects seven input params - year, month, day, hour, minute, second, offset
      my $year = $_[0] - 1900;
      my $mon = $_[1] - 1;
      my $day = $_[2];
      my $hour = $_[3] ? $_[3] : 0;
      my $min = $_[4] ? $_[4] : 0;
      my $sec = $_[5] ? $_[5] : 0;
      my $offset = $_[6] ? $_[6] : 0;
     
      # convert to seconds since epoch, then adjust for the $offset, which is also in seconds
      # we do this so we don't have to do fancy date arithmetic - we can just subtract one seconds
      # value from the other seconds value
      my $starttime = timelocal($sec,$min,$hour,$day,$mon,$year) - $offset;
     
      # convert back to component parts now that we've adjusted for offset
      # this gives us the components which represent the GMT time for the local time that was entered
      # on the command line
      ($sec,$min,$hour,$day,$mon,$year) = localtime($starttime);
     
      # format the date string, padding with zeros if needed
      return sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $day, $hour, $min, $sec);
      }
     
      &lt;/%perl&gt;
     
</syntaxhighlight>


5. Restart your server (unless you are using SetDevelMode, in which case restart is unnecessary).
5. Restart your server (unless you are using SetDevelMode, in which case restart is unnecessary).


'''TWEAKS FOR RT4 COMPATIBILITY'''
## TWEAKS FOR RT4 COMPATIBILITY


* Skip installation steps 1 and 2 above. (RT4 uses a different mechanism for building menus and navigation. More on that later.)
* Skip installation steps 1 and 2 above. (RT4 uses a different mechanism for building menus and navigation. More on that later.)
* Skip installation step 3. (This functionality is now built into the SelectQueue element included in RT4, so a separate element is no longer needed.)
* Skip installation step 3. (This functionality is now built into the SelectQueue element included in RT4, so a separate element is no longer needed.)
* Install the TimeWorkedReport.html as instructed in step 4, making the following modifications:
* Install the TimeWorkedReport.html as instructed in step 4, making the following modifications:
** Line 9  
** Line 9
*** currently begins with <code>&lt;&amp; /Tools/Reports/Elements/Tabs...</code>
*** currently begins with <code>&lt;&amp; /Tools/Reports/Elements/Tabs...</code>
*** Replace with <code>&lt;&amp; /Elements/Tabs &amp;&gt;</code>
*** Replace with <code>&lt;&amp; /Elements/Tabs &amp;&gt;</code>
Line 454: Line 472:
*** currently begins with <code>&lt;&amp; /Elements/SelectMultiQueue...</code>
*** currently begins with <code>&lt;&amp; /Elements/SelectMultiQueue...</code>
*** Replace with <code>&lt;&amp; /Elements/SelectQueue, Multiple =&gt; 1, Name =&gt; 'queues', Default =&gt; ($queues) ? $queues : &#39;&#39; &amp;&gt;</code>
*** Replace with <code>&lt;&amp; /Elements/SelectQueue, Multiple =&gt; 1, Name =&gt; 'queues', Default =&gt; ($queues) ? $queues : &#39;&#39; &amp;&gt;</code>
* Restart server as instructed in step 5. At this point, you should be able to access the report by going directly to its url ($WebBaseURL/Tools/Reports/TimeWorkedReport.html). Next, we'll add it to the Tools menu.
* Restart server as instructed in step 5. At this point, you should be able to access the report by going directly to its url ($WebBaseURL/Tools/Reports/TimeWorkedReport.html). Next, we'll add it to the Tools menu.
* Create the folder $RT_HOME/local/html/Callbacks/TimeWorkedReport/Elements/Tabs
* Create the folder $RT_HOME/local/html/Callbacks/TimeWorkedReport/Elements/Tabs
* In that folder, create a file called Privileged with the following contents:
* In that folder, create a file called Privileged with the following contents:
<code>
 
<syntaxhighlight lang="perl" line="1" >
  &lt;%init&gt;
  &lt;%init&gt;
  my $tools = Menu()-&gt;child('tools');
  my $tools = Menu()-&gt;child('tools');
Line 465: Line 484:
  $Actions =&gt; undef
  $Actions =&gt; undef
  &lt;/%args&gt;
  &lt;/%args&gt;
</code>
</syntaxhighlight>
 
 
* Restart the server again as instructed in step 5.
* Restart the server again as instructed in step 5.


'''Much below this line needs cleanup and dates to when this was a standalone CLI script'''
'''Much below this line needs cleanup and dates to when this was a standalone CLI script'''


'''GENERAL IMPLEMENTATION STRATEGY'''
## GENERAL IMPLEMENTATION STRATEGY


I took a transaction-based approach. I use a SearchBuilder to grab all of the transactions that took place in the time period of interest (see TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES below for time zone issue discussion). I then look at each one and go through the following workflow:
I took a transaction-based approach. I use a SearchBuilder to grab all of the transactions that took place in the time period of interest (see TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES below for time zone issue discussion). I then look at each one and go through the following workflow:
Line 491: Line 512:
  6. Increment the time worked value (see INCONSISTENT RECORDING OF TIME WORKED below
  6. Increment the time worked value (see INCONSISTENT RECORDING OF TIME WORKED below
     for issues here).
     for issues here).


'''TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES'''
## TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES


Internally in the database, RT stores time values as gmtime. This has implications for this script, which are best illustrated by an example.
Internally in the database, RT stores time values as gmtime. This has implications for this script, which are best illustrated by an example.
Line 507: Line 527:
In sum, if you want all the time worked for a week, let's say, from Sunday, August 9th through Saturday, August 15th (inclusive), if you use the start and end values "2009-08-09" and "2009-08-16" (Note: 16 not 15!) this script will do the right thing. To be more clear, you might use "2009-08-09" and "2009-08-15 23:59:59", but I am lazy and don't mind counting the first second of the 16th as part of the 15th. :-)
In sum, if you want all the time worked for a week, let's say, from Sunday, August 9th through Saturday, August 15th (inclusive), if you use the start and end values "2009-08-09" and "2009-08-16" (Note: 16 not 15!) this script will do the right thing. To be more clear, you might use "2009-08-09" and "2009-08-15 23:59:59", but I am lazy and don't mind counting the first second of the 16th as part of the 15th. :-)


'''INCONSISTENT RECORDING OF TIME WORKED'''
## INCONSISTENT RECORDING OF TIME WORKED


Time can be entered on tickets in two main ways.
Time can be entered on tickets in two main ways.
Line 513: Line 533:
  1. Putting a value in the Time Worked field as part of a Comment or Correspondence transaction.
  1. Putting a value in the Time Worked field as part of a Comment or Correspondence transaction.
  2. Directly editing the Time Worked field as part of a Basic or Jumbo ticket update.
  2. Directly editing the Time Worked field as part of a Basic or Jumbo ticket update.


Unfortunately, the way that the time gets recorded is different for each scenario.
Unfortunately, the way that the time gets recorded is different for each scenario.

Latest revision as of 14:56, 1 July 2016

    1. SUMMARY

Update

This contribution has now been properly packaged for RT as an extension. It has been updated and configured for RT 4. Refer to the link in See Also.

This report allows the user to specify a datetime range and one or more queues, and displays the time worked for each ticket in the selected queue(s) which has > 0 minutes time worked in the datetime range. It will show non-superusers their own report and superusers a report of all users.

It also will allow superusers to organize the report by ticket, with a breakdown of the contributions to that ticket per-person, rather than the default by-person organization.

Written by Fran Fabrizio, fabrizio -at- uab -dot- edu.

    1. REQUIREMENTS

This is known to work with RT 3.8.5. The changes needed to make it work with RT 4.0.x are outlined below.

    1. KNOWN BUGS

At least two users have reported a problem with RT 3.6.x and errors related to undefined principal within the HasRight sub of Queue_Overlay.pm. The RT developers changed HasRight between 3.6 and 3.8 and the code changes seem centered around undefined principals, so if anyone knows what this is and how I can code around it in a way that would work for 3.6, please contact me.

    1. SEE ALSO

http://search.cpan.org/dist/RT-Extension-ActivityReports/

Updated version below:

https://metacpan.org/pod/RT::Extension::TimeWorkedReport

If you upgrade to the new version and had the old one previously instaled, you will need to remove the files you created under ../local both for html and callbacks, and then clear the mason object cache in order for the new one to start working.

  1. TODO

This extension is still in active development. Currently, the TODO list is:

  • Figure out a better way to format this wiki page and include the code here (possible line break issues, careful when cutting and pasting)
  • Package this as an extension with an installer
  • Clean up the interface (align the form fields better, create a prettier report)
  • Create a new role, something like TimeWorkedManager, rather than only showing the all-users report to SuperUsers
  • Allow users to control extension behavior via RT's Configuration area
    1. FEEDBACK

If you have other suggestions/requests, please email me at fabrizio -at- uab -dot- edu. This is still early in development; expect rough edges.

    1. INSTALLATION

1. Copy $RT_HOME/share/html/Tools/Reports/index.html to $RT_HOME/local/html/Tools/Reports/index.html

Edit $RT_HOME/local/html/Tools/Reports/index.html and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 56 of the file):

 D => {
     title       => loc('Time Worked Report'),
     path        => '/Tools/Reports/TimeWorkedReport.html',
     description => loc('A Time Worked Report'),
 },

You may need to change the "D" to the next available letter, depending on other mods that you may have made.

2. Copy $RT_HOME/share/html/Tools/Reports/Elements/Tabs to $RT_HOME/local/html/Tools/Reports/Elements/Tabs

Edit $RT_HOME/local/html/Tools/Reports/Elements/Tabs and add the following lines to the anonymous hash pointed to by the $tabs variable (on or around line 55 of that file):

 d =&gt; {
     title =&gt; loc('Time Worked Report'),
     path  =&gt; 'Tools/Reports/TimeWorkedReport.html',
 },

Again, you may need to change the "d" to the next available letter depending on other mods you may have made.

3. Create the file $RT_HOME/local/html/Elements/SelectMultiQueue with the following content:

      %# BEGIN BPS TAGGED BLOCK {{{
      %#
      %# COPYRIGHT:
      %#
      %# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
      %#                                          &lt;jesse@bestpractical.com&gt;
      %#
      %# (Except where explicitly superseded by other copyright notices)
      %#
      %#
      %# LICENSE:
      %#
      %# This work is made available to you under the terms of Version 2 of
      %# the GNU General Public License. A copy of that license should have
      %# been provided with this software, but in any event can be snarfed
      %# from www.gnu.org.
      %#
      %# This work is distributed in the hope that it will be useful, but
      %# WITHOUT ANY WARRANTY; without even the implied warranty of
      %# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
      %# General Public License for more details.
      %#
      %# You should have received a copy of the GNU General Public License
      %# along with this program; if not, write to the Free Software
      %# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
      %# 02110-1301 or visit their web page on the internet at
      %# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
      %#
      %#
      %# CONTRIBUTION SUBMISSION POLICY:
      %#
      %# (The following paragraph is not intended to limit the rights granted
      %# to you to modify and distribute this software under the terms of
      %# the GNU General Public License and is only of importance to you if
      %# you choose to contribute your changes and enhancements to the
      %# community by submitting them to Best Practical Solutions, LLC.)
      %#
      %# By intentionally submitting any modifications, corrections or
      %# derivatives to this work, or any other work intended for use with
      %# Request Tracker, to Best Practical Solutions, LLC, you confirm that
      %# you are the copyright holder for those contributions and you grant
      %# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
      %# royalty-free, perpetual, license to use, copy, create derivative
      %# works based on those contributions, and sublicense and distribute
      %# those contributions and any derivatives thereof.
      %#
      %# END BPS TAGGED BLOCK }}}
      % if ($Lite) {
      %     my $d = new RT::Queue($session{'CurrentUser'});
      %     $d-&gt;Load($Default);
      &lt;input name="&lt;%$Name%&gt;" size="25" value="&lt;%$d-&gt;Name%&gt;" class="&lt;%$Class%&gt;" /&gt;
      % }
      % else {
      %     # $Default will be an arrayref if multiple queues are selected, or a
      %     # scalar if 0-1 queues are selected.  Hence, this ugly processing logic.
      %     my %selected;
      %     if (ref $Default) {
      %       for (@$Default) {
      %         $selected{$_} = 1;
      %       }
      %     } else {
      %       $selected{$Default} = 1;
      %     }
      &lt;select name="&lt;%$Name%&gt;" &lt;% ($OnChange) ? 'onchange="'.$OnChange.'"' : '' |n %&gt; class="  &lt;%$Class%&gt;" MULTIPLE&gt;
      %     if ($ShowNullOption) {
        &lt;option value=""&gt;-&lt;/option&gt;
      %     }
      %     for my $queue (@{$session{$cache_key}}) {
        &lt;option value="&lt;% ($NamedValues ? $queue-&gt;{Name} : $queue-&gt;{Id}) %&gt;"
      
      %# if ($queue-&gt;{Id} eq ($Default||'') || $queue-&gt;{Name} eq ($Default||'')) {
      % if($selected{$queue-&gt;{Id}}) {
       selected="selected"
      % }
      
      &gt;
          &lt;%$queue-&gt;{Name}%&gt;
      
      %             if ($Verbose and $queue-&gt;{Description}) {
          (&lt;%$queue-&gt;{Description}%&gt;)
      %             }
        &lt;/option&gt;
      %     }
      &lt;/select&gt;
      % }
      &lt;%args&gt;
      $CheckQueueRight =&gt; 'CreateTicket'
      $ShowNullOption =&gt; 1
      $ShowAllQueues =&gt; 1
      $Name =&gt; undef
      $Verbose =&gt; undef
      $NamedValues =&gt; 0
      $Default =&gt; 0
      $Lite =&gt; 0
      $OnChange =&gt; undef
      $Class =&gt; 'select-queue'
      &lt;/%args&gt;
      &lt;%init&gt;
      my $cache_key = "SelectQueue---"
                      . $session{'CurrentUser'}-&gt;Id
                      . "---$CheckQueueRight---$ShowAllQueues";
      
      if (not defined $session{$cache_key} and not $Lite) {
          my $q = new RT::Queues($session{'CurrentUser'});
          $q-&gt;UnLimit;
      
          while (my $queue = $q-&gt;Next) {
              if ($ShowAllQueues || $queue-&gt;CurrentUserHasRight($CheckQueueRight)) {
                  push @{$session{$cache_key}}, {
                      Id          =&gt; $queue-&gt;Id,
                      Name        =&gt; $queue-&gt;Name,
                      Description =&gt; $queue-&gt;Description,
                  };
              }
          }
      }
      &lt;/%init&gt;

This is a very slightly modified form of SelectQueue which ships with RT in the same directory. You can diff the two to see what I have changed.

4. Create the file $RT_HOME/local/html/Tools/Reports/TimeWorkedReport.html with the following content:

       &lt;%args&gt;
       $startdate =&gt; undef
       $enddate   =&gt; undef
       $queues    =&gt; undef
       $byticket  =&gt; undef
      &lt;/%args&gt;
      
      &lt;&amp; /Elements/Header, Title =&gt; $title &amp;&gt;
      &lt;&amp; /Tools/Reports/Elements/Tabs, current_tab =&gt; 'Tools/Reports/TimeWorkedReport.html', Title  =&gt; $title &amp;&gt;
      &lt;hr&gt;
      
      &lt;%init&gt;
      my ($start_date, $end_date, $effective_end_date, $title);
      
      $title = loc('Time worked report');
      
      $start_date = RT::Date-&gt;new($session{'CurrentUser'});
      $end_date   = RT::Date-&gt;new($session{'CurrentUser'});
      
      # If we have a value for start date, parse it into an RT::Date object
      if ($startdate) {
        $start_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $startdate);
        # And then get it back as an ISO string for display purposes, in the form field and
        # report header
        $startdate = $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
      }
      
      # Same treatment for end date
      if ($enddate) {
        $end_date-&gt;Set(Format =&gt; 'unknown', Value =&gt; $enddate);
        $enddate = $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server');
      }
      
      &lt;/%init&gt;
      
      &lt;form method="post" action="TimeWorkedReport.html"&gt;
       &lt;br /&gt;
        &lt;&amp;|/l&amp;&gt;Start date&lt;/&amp;&gt;:
        &lt;&amp; /Elements/SelectDate, Name =&gt; 'startdate', Default =&gt; ($startdate) ?  $start_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
        (report will start from midnight on this day unless you indicate otherwise)
       &lt;br /&gt;
        &lt;&amp;|/l&amp;&gt;End date&lt;/&amp;&gt;:
        &lt;&amp; /Elements/SelectDate, Name =&gt; 'enddate', Default =&gt; ($enddate) ?  $end_date-&gt;AsString(Format =&gt; 'ISO', Timezone =&gt; 'server') : ''&amp;&gt;
        (report will -not- be inclusive of this day unless you change the time from midnight)
       &lt;br /&gt;
        &lt;&amp;|/l&amp;&gt;Queues&lt;/&amp;&gt;:
        &lt;&amp; /Elements/SelectMultiQueue, Name =&gt; 'queues', Default =&gt; ($queues) ? $queues : ''&amp;&gt;
       &lt;br /&gt;
        &lt;&amp; /Elements/Checkbox, Name =&gt; 'byticket', Default =&gt; ($byticket) ? 'checked' : ''&amp;&gt;
        Organize report by ticket instead of by person
      &lt;&amp; /Elements/Submit&amp;&gt;
      
      &lt;/form&gt;
      
      
      &lt;%perl&gt;
      # TimeWorkedReport
      # Version 0.04  2009-09-28
      #
      # Fran Fabrizio, UAB CIS, fran@cis.uab.edu
      
      use strict;
      
      # if we are just getting here and the form values are empty, we are done
      if (!$startdate || !$enddate) {
        return;
      }
      
      # get the queue object(s)
      my $queuesobj = new RT::Queues($session{CurrentUser});
      my ($queuelist, %queuesofinterest);
      
      # The user's choice of queues will come in from the web form in the $queues variable, which is
      # mapped to the SELECT field on the web interface for the report.  Unfortunately, if the user
      # chooses just one queue, $queues will have a scalar value, but if the user chooses multiple
      # queues, it will be an arrayref.  So we need to check for each case and process differently.
      #
      # What we want to construct is the %queuesofinterest simple lookup hash which defines a key
      # that is the queue ID for each queue selected, and the $queuelist string, which is just for
      # displaying the list of queues in the report header
      $queues = [ $queues ] unless ref($queues);
     
      for (@$queues) {
        $queuesobj->Limit(FIELD => "Id", OPERATOR => "=", VALUE => $_, ENTRYAGGREGATOR => "OR");
        $queuesofinterest{$_} = 1;
      }
      $queuelist = join ", ", map {$_->Name} @{$queuesobj->ItemsArrayRef};
      
      # hash to hold statistics
      # %stats will be a multilevel hash - first level keys are the usernames, second level keys are
      # the ticket IDs, and for each ticket, we store an anonymous hash with keys Subject and  TimeWorked
      # (this implies that a single ticket can live under two+ users if they both worked the ticket)
      my %stats;
      
      # Get a new transactions object to hold transaction search results for this ticket
      my $trans = new RT::Transactions($session{'CurrentUser'});
      
      # only in the period of interest
      $trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&gt;', VALUE =&gt; $startdate);
      $trans-&gt;Limit(FIELD =&gt; 'Created', OPERATOR =&gt; '&lt;', VALUE =&gt; $enddate, ENTRYAGGREGATOR =&gt;  'AND');
      
      # now start counting all the TimeTaken by examining transactions associated with this ticket
      while (my $tr = $trans-&gt;Next) {
      
       # did this transaction take any time?  RT records this -either- in TimeTaken column or by
       # indicating "TimeWorked" in the Field column, depending on how the user inputted the time.
       if (($tr-&gt;TimeTaken != 0) || ($tr-&gt;Field &amp;&amp; $tr-&gt;Field eq 'TimeWorked')) {
         # Got a hot one - what ticket is this?
         my $t = new RT::Ticket($session{'CurrentUser'});
         $t-&gt;Load($tr-&gt;ObjectId);
      
         if (!$t) {
           # unable to retrieve a ticket for this transaction
           # hopefully we don't ever reach here!
           next;
         } else {
           # Is a queue selected and is this ticket in a queue we care about?
           if ($queuelist && !$queuesofinterest{$t-&gt;Queue}) {
             next;
           }
         }
      
         # If this is time logged by user RT_System, it's the result of a ticket merge
         # In order to avoid double-counting minutes in --byticket mode, or the less serious
         # issue of displaying a report for user RT_System in normal mode, we skip this entirely
         if ($tr-&gt;CreatorObj-&gt;Name eq 'RT_System') {
           next;
         }
      
         # we've got some time to account for
      
         # is this the first time this person is charging time to this ticket?
         # if so, add this ticket subject to the data structure
         if (!exists($stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject})) {
           $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{Subject} = $t-&gt;Subject;
         }
      
         if ($tr-&gt;TimeTaken != 0) {
           # this was a comment or correspondence where the user also added some time worked
           # value of interest appears in Transaction's TimeTaken column
           $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;TimeTaken;
         } else {
           # this was a direct update of the time worked field from the Basics or Jumbo ticket update page
           # values of interest appear in Transaction's OldValue and NewValue columns
           # RT does not use the TimeTaken column in this instance.
           $stats{$tr-&gt;CreatorObj-&gt;Name}{$t-&gt;id}{TimeWorked} += $tr-&gt;NewValue - $tr-&gt;OldValue;
         }
       }
      }
      
      # report output starts here
      # output:
      #  normal user: their own time worked report, most worked ticket to least worked ticket
      #  superuser:   everyone's time worked report, in username alpha order, then by most worked to least worked
      #  superuser+byticket: most worked ticket first, with everyone's contribution ranked by  biggest contribution to smallest
      
      print "&lt;h2&gt;TIME WORKED REPORT FOR QUEUE(S) " . $queuelist . "&lt;/h2&gt;";
      print "&lt;h3&gt;Date Range: $startdate TO $enddate&lt;/h3&gt;";
      if ($byticket) {
        print "&lt;h3&gt;Organized by Ticket&lt;/h3&gt;";
      }
      print "&lt;hr&gt;";
      
      # if this person is not a superuser, we should only show them the report for themselves
      # which means we should remove all keys from %stats except their own username
      if (!($session{'CurrentUser'}-&gt;HasRight(Right =&gt; 'SuperUser', Object =&gt; $RT::System))) {
        my %tempstats;
        $tempstats{$session{CurrentUser}-&gt;Name} = $stats{$session{CurrentUser}-&gt;Name};
        %stats = %tempstats;
      }
      
      if ($byticket) {
        # if we're going to organize this by ticket, we need to transform the data first
        # HAVE ENTRIES LIKE:  $stats{JoeUser}{12345}{TimeWorked} = 150
        #                     $stats{JoeUser}{12345}{Subject} = "Fix the Fubar Widget"
        # WANT ENTRIES LIKE:  $tstats{12345}{TotalTime} = 250
        #                     $tstats{12345}{Subject} = "Fix the Fubar Widget"
        #                     $tstats{12345}{People}{JoeUser} = 150
        #                     $tstats{12345}{People}{JaneDoe} = 100
      
        my %tstats;
        for my $person (keys %stats) {
          for my $tid (keys %{$stats{$person}}) {
            # grab the subject line if you don't have it already
            if (!exists($tstats{$tid}{Subject})) {
              $tstats{$tid}{Subject} = $stats{$person}{$tid}{Subject};
            }
            # now increment total time for this ticket
            $tstats{$tid}{TotalTime} += $stats{$person}{$tid}{TimeWorked};
            # and record this user's contribution to this ticket
            $tstats{$tid}{People}{$person} = $stats{$person}{$tid}{TimeWorked};
          }
        }
      
        # Now emit the report
        for my $tid (sort {$tstats{$b}{TotalTime} &lt;=&gt; $tstats{$a}{TotalTime}} keys %tstats) {
          my $subject = $tstats{$tid}{Subject};
          print "&lt;H3&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/H3&gt;";
          print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
          printf("&lt;TR&gt;&lt;TH WIDTH=30&gt;&lt;/TH&gt;&lt;TH&gt;%dm&lt;/TH&gt;&lt;TH&gt;%.1fh&lt;/TH&gt;&lt;TH&gt;TOTAL TIME&lt;/TH&gt;&lt;/TR&gt;",  $tstats{$tid}{TotalTime},($tstats{$tid}{TotalTime} / 60));
          for my $person (sort {$tstats{$tid}{People}{$b} &lt;=&gt; $tstats{$tid}{People}{$a}} keys %{$tstats{$tid}{People}}) {
            my $minutes = $tstats{$tid}{People}{$person};
            printf("&lt;TR&gt;&lt;TD&gt;&lt;/TD&gt;&lt;TD&gt;%dm&lt;/TD&gt;&lt;TD&gt;%.1fh&lt;/TD&gt;&lt;TD&gt;%s&lt;/TD&gt;&lt;/TR&gt;",$minutes,($minutes /60),$person);
          }
          print "&lt;/TABLE&gt;";
        }
      } else {
        # the existing %stats data structure is perfect for the default report, no data transform  needed
        for my $person (sort keys %stats) {
          # get the person object, so we can get the FriendlyName to use as header
          my $personobj = new RT::User($session{CurrentUser});
          $personobj-&gt;Load($person);
      
          print "&lt;h3&gt;" . $personobj-&gt;FriendlyName . "&lt;/h3&gt;";
          print "&lt;TABLE BORDER=0 CELLSPACING=5&gt;";
          print "&lt;TR&gt;&lt;TH&gt;MINUTES&lt;/TH&gt;&lt;TH&gt;HOURS&lt;/TH&gt;&lt;TH&gt;TICKET&lt;/TH&gt;&lt;/TR&gt;";
          my $totalMinutes = 0;
          for my $tid (sort {$stats{$person}{$b}{TimeWorked} &lt;=&gt; $stats{$person}{$a}{TimeWorked}}  keys %{$stats{$person}}) {
            my $minutes = $stats{$person}{$tid}{TimeWorked};
            my $subject = $stats{$person}{$tid}{Subject};
            print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;${minutes}m&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;" . sprintf("%.1fh",($minutes/60)) . "&lt;/TD&gt;" .
                      "&lt;TD&gt;&lt;A TARGET=\"_TimeWorked\" HREF=\"/Ticket/Display.html?id=$tid\"&gt;$tid:  $subject&lt;/A&gt;&lt;/TD&gt;&lt;/TR&gt;";
            $totalMinutes += $minutes;
          }
          print "&lt;TR&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;${totalMinutes}m&lt;/B&gt;&lt;/TD&gt;&lt;TD ALIGN=RIGHT&gt;&lt;B&gt;" . sprintf("%.1fh",($totalMinutes/60)) . "&lt;/B&gt;&lt;/TD&gt;&lt;TD&gt;&lt;B&gt;TOTALS&lt;/B&gt;&lt;/TD&gt;&lt;/TR&gt;";
          print "&lt;/TABLE&gt;";
        }
      }
      
      ##### helper functions below
      
      sub form_date_string {
       # expects seven input params - year, month, day, hour, minute, second, offset
       my $year = $_[0] - 1900;
       my $mon = $_[1] - 1;
       my $day = $_[2];
       my $hour = $_[3] ? $_[3] : 0;
       my $min = $_[4] ? $_[4] : 0;
       my $sec = $_[5] ? $_[5] : 0;
       my $offset = $_[6] ? $_[6] : 0;
      
       # convert to seconds since epoch, then adjust for the $offset, which is also in seconds
       # we do this so we don't have to do fancy date arithmetic - we can just subtract one seconds
       # value from the other seconds value
       my $starttime = timelocal($sec,$min,$hour,$day,$mon,$year) - $offset;
      
       # convert back to component parts now that we've adjusted for offset
       # this gives us the components which represent the GMT time for the local time that was entered
       # on the command line
       ($sec,$min,$hour,$day,$mon,$year) = localtime($starttime);
      
       # format the date string, padding with zeros if needed
       return sprintf("%04d-%02d-%02d %02d:%02d:%02d", ($year+1900), ($mon+1), $day, $hour, $min, $sec);
      }
      
      &lt;/%perl&gt;

5. Restart your server (unless you are using SetDevelMode, in which case restart is unnecessary).

    1. TWEAKS FOR RT4 COMPATIBILITY
  • Skip installation steps 1 and 2 above. (RT4 uses a different mechanism for building menus and navigation. More on that later.)
  • Skip installation step 3. (This functionality is now built into the SelectQueue element included in RT4, so a separate element is no longer needed.)
  • Install the TimeWorkedReport.html as instructed in step 4, making the following modifications:
    • Line 9
      • currently begins with <& /Tools/Reports/Elements/Tabs...
      • Replace with <& /Elements/Tabs &>
    • Line 47
      • currently begins with <& /Elements/SelectMultiQueue...
      • Replace with <& /Elements/SelectQueue, Multiple => 1, Name => 'queues', Default => ($queues) ? $queues : '' &>
  • Restart server as instructed in step 5. At this point, you should be able to access the report by going directly to its url ($WebBaseURL/Tools/Reports/TimeWorkedReport.html). Next, we'll add it to the Tools menu.
  • Create the folder $RT_HOME/local/html/Callbacks/TimeWorkedReport/Elements/Tabs
  • In that folder, create a file called Privileged with the following contents:
 &lt;%init&gt;
 my $tools = Menu()-&gt;child('tools');
 $tools-&gt;child( timeworked =&gt; title =&gt; 'Time Worked Report', path =&gt; '/Tools/Reports/TimeWorkedReport.html', description =&gt; 'Time Worked Report' );
 &lt;/%init&gt;
 &lt;%args&gt;
 $Actions =&gt; undef
 &lt;/%args&gt;


  • Restart the server again as instructed in step 5.

Much below this line needs cleanup and dates to when this was a standalone CLI script

    1. GENERAL IMPLEMENTATION STRATEGY

I took a transaction-based approach. I use a SearchBuilder to grab all of the transactions that took place in the time period of interest (see TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES below for time zone issue discussion). I then look at each one and go through the following workflow:

1. Does this transaction represent a time worked modification?
   (see INCONSISTENT RECORDING OF TIME WORKED below for issues here)  If no,
   skip to next transaction.
2. Use this transaction's object ID to retrieve the associated ticket object.
   If this fails, report the error and skip to next transaction.
3. Is this ticket in a queue of interest? If no, skip to next transaction.
4. Was this transaction entered by the user RT_System?  If so, it is the result
   of a merge operation.  Don't count this time and skip to the next transaction.
   (The time will be captured elsewhere, under the original user that entered it,
   if it originally occurred in this time period of interest).

Ok, if we got this far, we have real time to account for.

5. If this is the first time we are seeing this person for this ticket, create the
   multilevel data structure to store ticket and time worked info.
6. Increment the time worked value (see INCONSISTENT RECORDING OF TIME WORKED below
   for issues here).
    1. TIME VALUES IN RT / HOW TO SPECIFY DATE RANGES

Internally in the database, RT stores time values as gmtime. This has implications for this script, which are best illustrated by an example.

I live in the US/Central time zone which is -18000 seconds, or -5 hours, off from GM time. I also like to work late at night. If I enter some time worked onto a ticket at 11pm on August 12th, or more formally, at "2009-08-12 23:00:00" in US Central time, that will be entered in the transaction table with a timestamp of "2009-08-13 04:00:00".

Now if I run this script with a startdate of "2009-08-12 00:00:00" and an enddate of "2009-08-12 23:59:59", I would reasonably expect to get all of the time I worked on August 12th, 2009. However, I would miss the last 5 hours in the day worth of transactions, because RT would have internally shifted the stored timestamp to a time in August 13th (which it already was, over in London!)

So, I assume that users of this script will be entering their time in their local location, and automatically adjust for this. In my case, this means when I enter a start or end date like "2009-08-12 00:00:00", this script will convert that to "2009-08-12 05:00:00".

Unrelated to this issue but also relevant to the area of time values, MySQL will treat the absence of the HH:MM:SS as 00:00:00.

In sum, if you want all the time worked for a week, let's say, from Sunday, August 9th through Saturday, August 15th (inclusive), if you use the start and end values "2009-08-09" and "2009-08-16" (Note: 16 not 15!) this script will do the right thing. To be more clear, you might use "2009-08-09" and "2009-08-15 23:59:59", but I am lazy and don't mind counting the first second of the 16th as part of the 15th. :-)

    1. INCONSISTENT RECORDING OF TIME WORKED

Time can be entered on tickets in two main ways.

1. Putting a value in the Time Worked field as part of a Comment or Correspondence transaction.
2. Directly editing the Time Worked field as part of a Basic or Jumbo ticket update.

Unfortunately, the way that the time gets recorded is different for each scenario.

For scenario 1, in the transaction table the transaction will store the new time worked in the TimeTaken field as minutes.

For scenario 2, in the transaction table the transaction will use the OldValue and NewValue fields to store the old and new values.

So, the script needs to check for both cases, and in scenario 2, has to do a little math to figure out how much time was added.