Software Freedom Law Center

root/trunk/antimatter/tim/Modules/SFLC/TimeTracker/Reports.pm

Revision 53, 34.0 kB (checked in by bkuhn, 9 months ago)
  • Added SFLC's internally developed tim bot released under AGPLv3
Line 
1 # Copyright (C) 2006, 2007   Software Freedom Law Center, Inc.
2 #  Author: Bradley M. Kuhn <bkuhn@softwarefreedom.org>
3 #
4 #  This software gives you freedom; it is licensed to you under version
5 #  3 of the GNU Affero General Public License.
6 #
7 #  This software is distributed WITHOUT ANY WARRANTY, without even the
8 #  implied warranties of MERCHANTABILITY and FITNESS FOR A PARTICULAR
9 #  PURPOSE.  See the GNU Affero General Public License for further
10 #  details.
11 #
12 # You should have received a copy of the GNU Affero General Public
13 # License, version 3 along with this software.  If not, see
14 # <http://www.gnu.org/licenses/>.
15 # Reports.pm                                                          -*- Perl -*-
16 #  Database module for SFLC time tracker
17
18 # NOTE: some of these reports assume an epoch of 1 February 2000, that
19 # should be fixed, OTOH, it was written that way because using earlier
20 # epocs was tooo slow when the DB got large.
21
22 package SFLC::TimeTracker::Reports;
23
24 use strict;
25 use warnings;
26
27
28 use Lingua::EN::Inflect qw ( PL PL_N PL_V);
29 require Exporter;
30 #use AutoLoader qw(AUTOLOAD);
31
32 our @ISA = qw(Exporter);
33
34 our @EXPORT_OK = ();
35
36 our @EXPORT = qw( );
37
38 our $VERSION = '0.01';
39
40 # FIXME: stop hard coding this everywhere
41
42 my @VALID_USERS = qw/user1 user2 user3/;
43
44 use Carp;
45
46 use Date::Manip;
47 use Lingua::EN::Inflect qw ( PL PL_N PL_V);
48
49 use SFLC::TimeTracker::Input;
50 use SFLC::TimeTracker::DB;
51
52 use Data::Dumper;
53
54 my($f_email, $f_client, $f_date, $f_matter, $f_user, $f_note, $f_hours);
55
56 # format DETAIL_REPORT_TOP =
57 # Client        Matter             Employee  Date         Hours   Note
58 # --------------------------------------------------------------------------------
59 # .
60
61 my $F_MATTER_LEN_DETAIL = 17;
62 format DETAIL_REPORT =
63 @<<<<<<<<<   @<<<<<<<<<<<<<<<<<  @<<<<<<<  @<<<<<<<<<<  @####.#  ^<<<<<<<<<<<<<<
64 $f_client,   $f_matter,          $f_user,  $f_date,     $f_hours,$f_note
65 ~~                                                               ^<<<<<<<<<<<<<<
66                                                                  $f_note
67 .
68
69
70 my $F_MATTER_LEN_SUMMARY = 21;
71 format SUMMARY_REPORT =
72 @<<<<<<<<<<<   @<<<<<<<<<<<<<<<<<<<<<  @<<<<<<<  @####.#  ^<<<<<<<<<<<<<<<<<<<<
73 $f_client,   $f_matter,          $f_user,  $f_hours,      $f_note
74 ~~                                                        ^<<<<<<<<<<<<<<<<<<<<
75                                                           $f_note
76 .
77
78 ###############################################################################
79 sub _BuildDataStructure ($$$$$$) {
80   my($db, $i_user, $i_startDate, $i_endDate, $i_client, $i_matter) = @_;
81   my(@users) = ($i_user =~ /^ALL$/i) ? $db->getUserList : ($i_user);
82
83   my @entries;
84   foreach my $userHandle (@users) {
85     push(@entries, $db->getEntriesInDateRange('main',
86                                       $userHandle, $i_startDate, $i_endDate));
87   }
88
89 # This code
90 #   my $filterRE = '^\s*/legal';
91 #   if ($i_client =~ /^\s*ALL\s*$/i) {
92 #     if ($i_matter =~ /^\s*ALL\s*$/i) {
93 #     } else {
94 #       $filterRE .= '/\S+/' . $i_matter;
95 #     }
96 #   } else {
97 #     if ($i_matter =~ /^\s*ALL\s*$/i) {
98 #       $filterRE .= '/' . $i_client;
99 #     } else {
100 #       $filterRE .= '/' . $i_client  . '/' . $i_matter;
101 #     }
102 #   }
103
104   my $filterRE;
105   $i_client = "legal/client/$i_client/"
106     if ($i_client ne "admin" and $i_client ne "tech" and
107         $i_client !~ /^\s*ALL\s*$/i);
108
109   if ($i_client =~ /^\s*ALL\s*$/i) {
110     if ($i_matter =~ /^\s*ALL\s*$/i) {
111       $filterRE = undef;
112     } else {
113       $filterRE = $i_matter .'\s*$';
114     }
115   } else {
116     if ($i_matter =~ /^\s*ALL\s*$/i) {
117       $filterRE = '^\s*/' . $i_client;
118     } else {
119       $filterRE = '^\s*/' . $i_client  . $i_matter .'\s*$';
120     }
121   }
122
123   my($lastTopLevel, $lastClient, $lastMatter, $lastUser) = ("", "", "", "");
124   my $lastDate = '1975-01-01';
125
126   my $firstTime = 1;
127
128   my %r;
129   foreach my $entry (@entries) {
130     my $category = $entry->get('category')->prettyPrint;
131     # Skip categories we don't want
132     next if defined $filterRE and $category !~ /$filterRE/i;
133
134     # Find new client/matter
135     my($newTopLevel, $newClient, $newMatter) = ("BORKEN", "BORKEN", "BORKEN");
136     if ($category =~ s/^\/legal\/client\///) {
137       $newTopLevel = "legal";
138       if ($category =~ s/^(someone\/withsubmatters)\///) {
139         $newClient = $1;
140       } else {
141         $category =~ s/^([^\/]+)\///;
142         $newClient = $1;
143       }
144       $newMatter = $category;
145     } else {
146       $category =~ s/^\/([^\/]+)\///;
147       $newClient = $1;
148       $newMatter = $category;
149       $newTopLevel = $newClient;
150     }
151     $newTopLevel = 'other'
152     if ($newClient eq "admin" and
153         $newMatter =~ /^office\-chat|sick|vacation|holiday|break|lunch|personal$/);
154
155     my $newDate = $entry->get('dateOccurred');
156     my $newUser = $entry->get('userHandle');
157     die( "invalid date in " . $entry->get('id')) if not defined $newDate;
158     die( "invalid user in " . $entry->get('id')) if not defined $newUser;
159
160 #    print $entry->get('id'), ": \"$newTopLevel\", \"$newClient\", \"$newMatter\", \"$newUser\", \"$newDate\"\n";
161      if (not exists
162          $r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}) {
163        $r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}{hours}
164          = 0.0;
165        $r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}{notes}
166          = [];
167      }
168     my $h = Delta_Format($entry->get('amountTime'), 1, '%ht');
169     die ("bad amount in " . $entry->{id}) if (not defined $h or $h =~ /^\s*$/);
170     $r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}{hours} += $h;
171     my $n = $entry->get('note');
172     if (defined $n and $n !~ /^\s*$/) {
173       # Add the new note, $n, to the old list of notes, if it's not already in there
174       # in it.
175
176       unless (grep /$n/i,
177         @{$r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}{notes}}) {
178         push(@{$r{$newTopLevel}{$newClient}{$newMatter}{$newUser}{$newDate}{notes}},
179              $n);
180       }
181     }
182   }
183   return %r;
184 }
185 ###############################################################################
186 # $startDate and $endDate should already be in Date::Manip format
187 sub Detail ($$$$$$$$) {
188   my($db, $i_user, $i_startDate, $i_endDate, $i_client, $i_matter, $email,
189      $emailNote) = @_;
190
191   my $startPretty = UnixDate($i_startDate, '%F');
192   my $endPretty = UnixDate($i_endDate, '%F');
193
194   my $startSmall = UnixDate($i_startDate,  "%Y-%m-%d");
195   my $endSmall = UnixDate($i_endDate, "%Y-%m-%d");
196
197   open(DETAIL_REPORT, "|/usr/sbin/sendmail -f time\@example.org -t");
198 #  open(DETAIL_REPORT, ">$i_user.report");
199   print DETAIL_REPORT <<HEADER;
200 To: $email
201 From: Tim <time\@example.org>
202 Subject: Time Detail: $startSmall to $endSmall
203
204 $emailNote
205                            Time Detail Report
206 Report Criteria
207      Start Date: $startPretty
208      End   Date: $endPretty
209      Client:     $i_client
210      Matter:     $i_matter
211      Employee:   $i_user
212
213
214 Client       Matter              Employee  Date           Hours       Note
215 --------------------------------------------------------------------------------
216 HEADER
217
218
219   my(%r) = _BuildDataStructure($db, $i_user, $i_startDate, $i_endDate,
220                                $i_client, $i_matter);
221   my($overallTotal, $overallNoOther) = (0.0, 0.0);
222   foreach my $topLevel (sort { return -1 if $a eq "legal";
223                                return 1 if $b eq 'legal';
224                                return 1 if $a eq 'other';
225                                return -1 if $b eq 'other';
226                                return $a cmp $b; }
227                         keys %r) {
228     my $topLevelTotal = 0.0;
229     foreach my  $client (sort {
230                            return -1 if ($a =~ /^(?:admin|tech)/ and
231                                       $b !~ /^(?:admin|tech)/);
232                            return 1 if ($b =~ /^(?:admin|tech)/ and
233                                       $a !~ /^(?:admin|tech)/);
234                            return $a cmp $b;
235                          } keys %{$r{$topLevel}}) {
236       my $clientTotal = 0.0;
237       my $clientFirst = 1;
238       foreach my $matter (sort { $a cmp $b }
239                             keys %{$r{$topLevel}{$client}}) {
240         my $matterTotal = 0.0;
241         my $matterFirst = 1;
242         $clientFirst = 1; # Dan likes to see the client again at beginning
243                           # of each matter
244         foreach my $user
245           (sort { $a cmp $b } keys %{$r{$topLevel}{$client}{$matter}}) {
246             my $userTotal = 0.0;
247             my $userFirst = 1;
248             print  DETAIL_REPORT "\n"; $---;
249             foreach my $date
250               (sort { $a cmp $b }
251                keys %{$r{$topLevel}{$client}{$matter}{$user}}) {
252                 $f_matter = $matterFirst ? $matter : "";
253                 my $len = length($f_matter);
254                 $f_matter = ($len <= $F_MATTER_LEN_DETAIL) ? $f_matter
255                   : ( substr($f_matter, 0, 2) . "..." .
256                       substr($f_matter,
257                                       $len - ($F_MATTER_LEN_DETAIL - 4),$len));
258
259                 $f_client = $clientFirst ? $client : "";
260                 $f_user =   $userFirst   ? $user   : "";
261                 $f_date = $date;
262                 $f_hours = $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
263                 $f_note = join("; ",
264                      @{$r{$topLevel}{$client}{$matter}{$user}{$date}{notes}});
265                 write DETAIL_REPORT;
266
267                 $userTotal +=
268                   $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
269
270                 $userFirst = $matterFirst = $clientFirst = 0;
271               } # date
272             $matterTotal += $userTotal;
273
274             print  DETAIL_REPORT "\n"; $---;
275             $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
276 #            $f_client = $client; $f_matter = $matter; $f_user = $user;
277             $f_date = "TOTAL";
278             $f_hours = $userTotal;
279             write DETAIL_REPORT;
280
281
282           } # user
283         $clientTotal += $matterTotal;
284
285         print  DETAIL_REPORT "\n"; $---;
286         $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
287 #        $f_client = $client; $f_matter = $matter;
288         $f_user = "TOTAL";
289         $f_hours = $matterTotal;
290         write DETAIL_REPORT;
291         print  DETAIL_REPORT "\n"; $---;
292
293       } #matter
294       $topLevelTotal += $clientTotal;
295
296       print  DETAIL_REPORT "\n"; $---;
297       $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
298 #      $f_client = $client;
299       $f_matter = "TOTAL";
300       $f_hours = $clientTotal;
301       write DETAIL_REPORT;
302         print  DETAIL_REPORT "\n"; $---;
303
304
305     } #client
306     $overallTotal += $topLevelTotal;
307     $overallNoOther += $topLevelTotal unless $topLevel =~ /^other/;
308
309     $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
310     print  DETAIL_REPORT "\n"; $---;
311     $f_client = "TOT-$topLevel";
312     $f_hours = $topLevelTotal;
313     write DETAIL_REPORT;
314
315   } #toplevel
316   if ($overallTotal != $overallNoOther) {
317     print  DETAIL_REPORT "\n"; $---;
318     $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
319     $f_client = "TOT-NOOTHR";
320     $f_hours = $overallNoOther;
321     write DETAIL_REPORT;
322   }
323
324   print  DETAIL_REPORT "\n"; $---;
325   $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
326   $f_client = "TOTAL ALL";
327   $f_hours = $overallTotal;
328   write DETAIL_REPORT;
329   print  DETAIL_REPORT "\n"; $---;
330
331   close(DETAIL_REPORT);
332 }
333 ###############################################################################
334 # $startDate and $endDate should already be in Date::Manip format
335 sub Summary ($$$$$$$$) {
336   my($db, $i_user, $i_startDate, $i_endDate, $i_client, $i_matter, $email,
337      $emailNote) = @_;
338
339
340   my $startPretty = UnixDate($i_startDate, '%F');
341   my $endPretty = UnixDate($i_endDate, '%F');
342
343   my $startSmall = UnixDate($i_startDate,  "%Y-%m-%d");
344   my $endSmall = UnixDate($i_endDate, "%Y-%m-%d");
345
346   open(SUMMARY_REPORT, "|/usr/sbin/sendmail -f time\@example.org -t");
347 #  open(SUMMARY_REPORT, ">$i_user.report");
348   print SUMMARY_REPORT <<HEADER;
349 To: $email
350 From: Tim <time\@example.org>
351 Subject: Time Summary: $startSmall to $endSmall
352
353 $emailNote
354                            Time Summary Report
355 Report Criteria
356      Start Date: $startPretty
357      End   Date: $endPretty
358      Client:     $i_client
359      Matter:     $i_matter
360      Employee:   $i_user
361
362
363 Client         Matter                  Employee    Hours            Notes
364 -------------------------------------------------------------------------------
365 HEADER
366
367
368   my(%r) = _BuildDataStructure($db, $i_user, $i_startDate, $i_endDate,
369                                $i_client, $i_matter);
370   my($overallTotal, $overallNoOther) = (0.0, 0.0);
371   $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
372   foreach my $topLevel (sort { return -1 if $a eq "legal";
373                                return 1 if $b eq 'legal';
374                                return 1 if $a eq 'other';
375                                return -1 if $b eq 'other';
376                                return $a cmp $b; }
377                         keys %r) {
378     my $topLevelTotal = 0.0;
379     foreach my  $client (sort {
380                            return -1 if ($a =~ /^(?:admin|tech)/ and
381                                       $b !~ /^(?:admin|tech)/);
382                            return 1 if ($b =~ /^(?:admin|tech)/ and
383                                       $a !~ /^(?:admin|tech)/);
384                            return $a cmp $b;
385                          } keys %{$r{$topLevel}}) {
386       my $clientTotal = 0.0;
387       my $clientFirst = 1;
388       foreach my $matter (sort { $a cmp $b }
389                             keys %{$r{$topLevel}{$client}}) {
390         my $matterTotal = 0.0;
391         my $matterFirst = 1;
392         $clientFirst = 1;
393         foreach my $user
394           (sort { $a cmp $b } keys %{$r{$topLevel}{$client}{$matter}}) {
395             my $userTotal = 0.0;
396             my $userFirst = 1;
397             my $bigNote = "";
398             foreach my $date (keys %{$r{$topLevel}{$client}{$matter}{$user}}) {
399               foreach my $thisNote (
400                        @{$r{$topLevel}{$client}{$matter}{$user}{$date}{notes}}) {
401                 my $quotedThisNote = quotemeta $thisNote;
402                 $bigNote .= ($bigNote eq "") ? $thisNote : "; $thisNote"
403                   if $thisNote !~ /^\s*$/ and $bigNote !~ /$quotedThisNote/i;
404               }
405               $userTotal +=
406                 $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
407
408             } # date
409             $matterTotal += $userTotal;
410
411             $f_matter = $matterFirst ? $matter : "";
412             my $len = length($f_matter);
413             $f_matter = ($len <= $F_MATTER_LEN_SUMMARY) ? $f_matter
414               : ( substr($f_matter, 0, 2) . "..." .
415                   substr($f_matter,
416                          $len - ($F_MATTER_LEN_SUMMARY - 4),$len));
417
418             $f_client = $clientFirst ? $client : "";
419             $f_user =   $user;
420             $f_hours = $userTotal;
421             $f_note = $bigNote;
422             write SUMMARY_REPORT;
423
424             $userFirst = $matterFirst = $clientFirst = 0;
425           } # user
426         $clientTotal += $matterTotal;
427
428         print  SUMMARY_REPORT "\n"; $---;
429         $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
430 #        $f_client = $client; $f_matter = $matter;
431         $f_user = "TOTAL";
432         $f_hours = $matterTotal;
433         write SUMMARY_REPORT;
434         print  SUMMARY_REPORT "\n"; $---;
435
436       } #matter
437       $topLevelTotal += $clientTotal;
438
439       $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
440 #      $f_client = $client;
441       $f_matter = "TOTAL";
442       $f_hours = $clientTotal;
443       write SUMMARY_REPORT;
444         print  SUMMARY_REPORT "\n"; $---;
445
446
447     } #client
448     $overallTotal += $topLevelTotal;
449     $overallNoOther += $topLevelTotal unless $topLevel =~ /^other/;
450
451     $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
452     print  SUMMARY_REPORT "\n"; $---;
453     $f_client = "TOT-$topLevel";
454     $f_hours = $topLevelTotal;
455     write SUMMARY_REPORT;
456     print  SUMMARY_REPORT "\n"; $---;
457
458   } #toplevel
459   if ($overallTotal != $overallNoOther) {
460     print  SUMMARY_REPORT "\n"; $---;
461     $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
462     $f_client = "TOT-NOOTHR";
463     $f_hours = $overallNoOther;
464     write SUMMARY_REPORT;
465   }
466
467   print  SUMMARY_REPORT "\n"; $---;
468   $f_date = $f_hours = $f_client = $f_matter = $f_user = $f_note = "";
469   $f_client = "TOTAL ALL";
470   $f_hours = $overallTotal;
471   write SUMMARY_REPORT;
472   print  SUMMARY_REPORT "\n"; $---;
473
474   close(SUMMARY_REPORT);
475 }
476 ###############################################################################
477 sub _FormatDeltaNice ($) {
478   my($amountTime) = @_;
479
480   my $formatted = (defined $amountTime) ?
481     Delta_Format($amountTime, 1,
482                  '%ht ' . PL_N("hour", $amountTime)) : "";
483   # If we are going to say "0.0 hours, just redo it in minutes
484   if ($formatted =~ /^\s*0.0+\s+hour/) {
485     $formatted = Delta_Format($amountTime, 1,
486                                '%mt ' . PL_N("minute", $amountTime));
487   }
488   # If we are going to say "0.0 minutes, just redo it in seconds
489   if ($formatted =~ /^\s*0.0+\s+minute/) {
490     $formatted = Delta_Format($amountTime, 2,
491                                '%st ' . PL_N("second", $amountTime));
492   }
493   $formatted =~ s/s\s*$//  if ($formatted =~ /^\s*1.0\s+/);
494   $formatted = "none" if $formatted =~ /^\s*$/;
495
496
497   return $formatted;
498 }
499 ###############################################################################
500 sub QuickSummary ($$$$$$$$$) {
501   my($db, $userHandle, $startDate, $endDate, $verbose, $email, $now,
502      $noteFilter, $showPending) = @_;
503
504   my @answers;
505   my $iWannaBeSedated = DateCalc($now, "24 hours ago");
506
507   for (my $day = $startDate; $day le $endDate;
508        $day = DateCalc($day, "+1 day")) {
509     push(@answers, "\n\n") if $email and $day ne $startDate;
510     push(@answers, "Status for " . UnixDate($day, ' %a, %b %E') . ":\n");
511
512     my(@completed) = $db->getEntriesOnDateInDB('main', $userHandle, $day);
513     my %cats;
514     foreach my $entry (sort  { $a->{id} cmp $b->{id} } @completed) {
515       my $category = $entry->get('category')->prettyPrint();
516       my $amount = $entry->get('amountTime');
517       $cats{$category} = '+0:0:0:0:0:0:0'
518         if not defined $cats{$category};
519
520       my $note = $entry->get('note');
521       next if (defined $noteFilter and
522                (not defined $note or $note !~ /$noteFilter/));
523
524
525       $cats{$category} = DateCalc($cats{$category}, $amount);
526       if ($verbose) {
527         push(@answers, "    ", $entry->get('id'), ": ",
528           _FormatDeltaNice($amount), "in $category" .
529          ((defined $note and $note !~ /^\s*$/) ? ", note: $note" : "") . "\n");
530       }
531     }
532     my $total = '+0:0:0:0:0:0:0';
533     foreach my $cat (sort {$a cmp $b} keys %cats) {
534       push(@answers, sprintf("    %s: %s\n", $cat,
535                              _FormatDeltaNice($cats{$cat})))
536         if not $verbose;
537       $total = DateCalc($total, $cats{$cat});
538     }
539     push(@answers, "\n") if $email;
540     push(@answers, ($total eq '+0:0:0:0:0:0:0')
541          ? "    No completed entries.\n" :
542               ("    Total time completed is " . _FormatDeltaNice($total)
543                . "\n"));
544   }
545
546   if ($showPending) {
547     my $pending = $db->getPendingEntriesByUserHandle($userHandle);
548
549     my(@pending) = values %{$pending};
550     if (@pending > 0) {
551       push(@answers, "\n\n") if $email;
552       push(@answers, "Pending Entries as of " .
553            UnixDate($now, '%i:%M%p (on %b %E)') . ": \n");
554     }
555     foreach my $entry (sort { $a->{id} cmp $b->{id} } @pending) {
556       my $category = $entry->get('category');
557       my $startTime = $entry->get('startTime');
558       my $endTime = $entry->get('endTime');
559       my $time = $startTime;
560       if (defined $time) {
561         my $oldTime = $time;
562         $time = UnixDate($oldTime, 'started at %i:%M%p');
563         $time .= UnixDate($oldTime, ' (on %b %E)')
564           if $email or ($oldTime lt $iWannaBeSedated);
565       } else {
566         $time = $entry->get('amountTime');
567         $time = "that lasted for " . _FormatDeltaNice($time);
568       }
569       my $answer = "    ";
570       $answer .= ($entry->{id} . ": ") if ($verbose);
571       $answer = "    Pending entry $time";
572       $answer .= $entry->needs('category') ?
573         " needs a category" : (" in " . $category->prettyPrint);
574       $answer .= $entry->needs('endTime') ? ' is still running.' : ".";
575       push(@answers, "$answer\n");
576     }
577   }
578   if ($email) {
579     my $startPretty = UnixDate($startDate, '%F');
580     my $endPretty = UnixDate($endDate, '%F');
581
582     my $startSmall = UnixDate($startDate,  "%Y-%m-%d");
583     my $endSmall = UnixDate($endDate, "%Y-%m-%d");
584
585     open(QUICK_SUMMARY_REPORT,
586          "|/usr/sbin/sendmail -f time\@example.org -t");
587     print QUICK_SUMMARY_REPORT <<HEADER;
588 To: $userHandle\@example.org
589 From: Tim <time\@example.org>
590 Subject: Time Status Report: $startSmall to $endSmall
591
592                             Time Status Report
593 Report Criteria
594      Start Date:  $startPretty
595      End   Date:  $endPretty
596 HEADER
597
598     print QUICK_SUMMARY_REPORT "     Note Filter: $noteFilter\n" if defined $noteFilter;
599     print QUICK_SUMMARY_REPORT "\n\n", @answers, "\n";
600     close QUICK_SUMMARY_REPORT;
601     my $msg = "Your status report " . ( ($startPretty eq $endPretty) ?
602              "for $startPretty" :
603              "for the dates from $startPretty to $endPretty (inclusive)" ) .
604            ( (defined $noteFilter) ? " with note filter, \"$noteFilter\", " :"") .
605               " has been emailed to you.";
606     (@answers) = ($? == 0) ? ($msg)
607                    : ("REPORT_THIS: An unexpected error ($!) prohibited me "
608                        . "from sending your status report via email");
609   }
610   return @answers;
611 }
612
613 ###############################################################################
614 sub CategoryTotals ($$$$$$;$$) {
615   my($db, $i_user, $i_startDate, $i_endDate, $email, $emailNote, $i_client,
616      $i_matter) = @_;
617
618   $i_client = 'ALL' if (not defined $i_client);
619   $i_matter = 'ALL' if (not defined $i_matter);
620
621   my(%r) = _BuildDataStructure($db, $i_user, $i_startDate, $i_endDate,
622                                $i_client, $i_matter);
623   my %userCats;
624
625   my $overheadCatsRE = '^(?:/admin/overhead)';
626   my $ignoredCatsRE = '^/other';
627
628   my $startPretty = UnixDate($i_startDate, '%F');
629   my $endPretty = UnixDate($i_endDate, '%F');
630
631   my $startSmall = UnixDate($i_startDate,  "%Y-%m-%d");
632   my $endSmall = UnixDate($i_endDate, "%Y-%m-%d");
633
634   if (defined $email) {
635     open(CAT_REPORT, "|/usr/sbin/sendmail -f time\@example.org -t");
636   } else {
637       *CAT_REPORT = *STDOUT;
638   }
639   $emailNote = "" unless defined $emailNote;
640
641   print CAT_REPORT <<HEADER;
642 To: $email
643 From: Tim <time\@example.org>
644 Subject: CategoryReport: $startSmall to $endSmall
645
646 $emailNote
647                            Category Totals Report
648 Report Criteria
649      Start Date: $startPretty
650      End   Date: $endPretty
651      Client:     $i_client
652      Matter:     $i_matter
653 HEADER
654   foreach my $topLevel(keys %r) {
655     foreach my $client (keys %{$r{$topLevel}}) {
656       foreach my $matter (keys %{$r{$topLevel}{$client}}) {
657         foreach my $user (keys %{$r{$topLevel}{$client}{$matter}}) {
658           foreach my $date (keys %{$r{$topLevel}{$client}{$matter}{$user}}) {
659             my $cat = "/$topLevel";
660             $cat .= "/$client" unless $topLevel eq $client;
661             $cat .= "/$matter";
662             $userCats{$user}{$cat} = 0 unless defined $userCats{$user}{$cat};
663             $userCats{$user}{$cat} +=
664               $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
665             if ($cat =~ m%$overheadCatsRE%) {
666               $userCats{$user}{'__TOTAL_OVERHEAD__'} +=
667                 $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
668             } elsif ($cat =~ m%$ignoredCatsRE%) {
669               $userCats{$user}{'__TOTAL_TIME_OFF__'} +=
670                 $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
671             } else {
672               $userCats{$user}{'__TOTAL_PROGRAM_ACTIVITY__'} +=
673                 $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
674             }
675           }
676         }
677       }
678     }
679   }
680
681   my %everyone;
682
683   my $ret = "";
684   foreach my $user (sort { $b cmp $a } keys %userCats) {
685     $ret .= "\n\n$user:\n";
686     my @finalCats;
687     foreach my $cat (sort { $userCats{$user}{$b} <=> $userCats{$user}{$a} }
688                      keys %{$userCats{$user}}) {
689       $everyone{$cat}  = 0 unless defined $everyone{$cat};
690       $everyone{$cat} += $userCats{$user}{$cat};
691       if ($cat =~ /^__TOTAL/) {
692         push(@finalCats, $cat);
693         next;
694       }
695       $ret .= sprintf("     %-60.60s: %7.1f\n", $cat, $userCats{$user}{$cat});
696     }
697     $ret .= "\n";
698     foreach my $cat (sort @finalCats) {
699       $ret .= sprintf("     %-60.60s: %7.1f\n", $cat, $userCats{$user}{$cat});
700     }
701   }
702   $ret .= "\n\nEVERYONE:\n";
703   my @finalCats;
704   foreach my $cat (sort { $everyone{$b} <=> $everyone{$a}} keys %everyone) {
705     if ($cat =~ /^__TOTAL/) {
706       push(@finalCats, $cat);
707       next;
708     }
709     $ret .= sprintf("     %-60.60s: %7.1f\n", $cat, $everyone{$cat});
710   }
711   $ret .= "\n";
712   foreach my $cat (sort @finalCats) {
713     $ret .= sprintf("     %-60.60s: %7.1f\n", $cat, $everyone{$cat});
714   }
715   print CAT_REPORT $ret;
716 }
717 ###############################################################################
718 sub PercentageOfMonth ($$) {
719   my($startDate, $endDate) = @_;
720   my $monthYear = UnixDate($startDate, "%B %Y");
721   my $isoDateTime = UnixDate($startDate, "%Y-%m-%d");
722   my $firstDate = ParseDate(UnixDate($startDate, "%Y-%m-01"));
723   die "PercentageOfMonth requires two dates in same month: $startDate, $endDate"
724     unless ($monthYear eq UnixDate($endDate, "%B %Y"));
725   my $daysInPeriod  = Delta_Format(DateCalc(ParseDate($isoDateTime),
726                         ParseDate("last day in $monthYear")), 0, "%dt");
727   my $daysInMonth = Delta_Format(DateCalc($firstDate,
728                                ParseDate("last day in $monthYear")),0, "%dt");
729   return $daysInPeriod / $daysInMonth;
730 }
731 ###############################################################################
732 #FIXME: EVIL EVIL EVIL HARD CODING!  It makes the baby Jesus cry.  For the love
733 #   of all that is holy, put this in LDAP!
734 my %START_DATES = (user1 => '2001-01-01', user2 => '2002-02-05');
735
736 my %PART_TIME_BOUNDRIES = (user2 => [ { startDate => ParseDate('2003-03-01'),
737                                      endDate   => ParseDate('2004-01-15'),
738                                      percentage => 0.5 }
739 ]);
740 my $FIRST_TWO_YEAR_ACCURAL = (13 * 8)/ 12;
741 my $THEREAFER_ACCURAL = (18 * 8)/ 12;
742 my $MAXIMUM_BANKED = 160;
743 ###############################################################################
744 sub PartTimeCheckAndCalc ($$$) {
745 # $currentDate is assumed to be in Date::Manip format
746   my($user, $currentDate, $accuralAmount) = @_;
747   # FIXME: this doesn't implement part-time months properly!!!!
748   #   It should technically figure out
749   my $percentage = 1;
750   foreach my $period (@{$PART_TIME_BOUNDRIES{$user}}) {
751     if ($currentDate ge $period->{startDate}) {
752       if ($currentDate le $period->{endDate}) {
753          return $period->{percentage} * $accuralAmount;
754       } elsif (UnixDate($currentDate, "%Y-%m") eq
755                UnixDate($period->{endDate}, "%Y-%m")) {
756         my $percentFullTime = PercentageOfMonth($period->{endDate},
757                                                 $currentDate);
758         return ($percentFullTime * $accuralAmount) +
759             ($period->{percentage} * (1.0 - $percentFullTime) * $accuralAmount);
760       }
761     }
762   }
763   return ($percentage * $accuralAmount);
764 }
765 ###############################################################################
766 sub Vacation ($$$$$)  {
767   my($db, $i_user, $i_endDate, $email, $emailNote) = @_;
768
769   my $endPretty = UnixDate($i_endDate, '%F');
770   my $endSmall = UnixDate($i_endDate, "%Y-%m-%d");
771
772   if (defined $email) {
773     open(VAC_REPORT, "|/usr/sbin/sendmail -f time\@example.org -t");
774   } else {
775       *VAC_REPORT = *STDOUT;
776       $email = "STDOUT";
777   }
778   $emailNote = "" unless defined $emailNote;
779
780   print VAC_REPORT <<HEADER;
781 To: $email
782 From: Tim <time\@example.org>
783 Subject: Time Off Remaining Report: ending $endSmall
784
785 $emailNote
786                       Time-Off Used/Remaining Report
787 Report Criteria
788      Employee:   $i_user
789      End   Date: $endPretty
790
791
792 HEADER
793
794   my %accruedMatrix;
795   my %totalsWithoutExpiration;
796   my %twoYearMark;
797   my(@users) = ($i_user eq "ALL") ? $db->getUserList() : ($i_user);
798   my %users; @users{@users} = @users;
799   delete $users{'aaronw'};
800   #I am evil
801   @users = keys %users;
802
803   # Normalize start dates
804   foreach my $user (@users) {
805     $START_DATES{$user} = ParseDate($START_DATES{$user});
806     my $err;
807     my $x = $twoYearMark{$user}{dateManipFormat} =
808       DateCalc($START_DATES{$user}, "+ 2 years", \$err);
809     $twoYearMark{$user}{yearMonth} =   UnixDate($x, "%Y-%m");
810     $twoYearMark{$user}{yearMonthDay} =   UnixDate($x, "%Y-%m-%d");
811     $totalsWithoutExpiration{$user} = 0.0;
812   }
813   my $err;
814
815   for (my $lastDayOfMonth = ParseDate('last day in February 2000'),
816        my $startOfMonth = ParseDate('2000-02-01');
817        $lastDayOfMonth le $i_endDate;
818        $startOfMonth = DateCalc($startOfMonth, "+ 1 month", \$err),
819        $lastDayOfMonth = ParseDate(UnixDate($startOfMonth,
820                                             "last day in %B %Y"))) {
821     my $yearMonth = UnixDate($lastDayOfMonth, "%Y-%m");
822     foreach my $user (@users) {
823       my $startDateYearMonth = UnixDate($START_DATES{$user}, "%Y-%m");
824       if ($startDateYearMonth gt $yearMonth) {
825         $totalsWithoutExpiration{$user} =
826           $accruedMatrix{$lastDayOfMonth}{$user}{accruedThisMonth} = 0;
827         next;
828       }
829       my $hoursToAdd = 0;
830       if ($twoYearMark{$user}{yearMonth} lt $yearMonth) {
831         $hoursToAdd = $THEREAFER_ACCURAL;
832       } elsif ($twoYearMark{$user}{yearMonth} eq $yearMonth) {
833         my $percent = $accruedMatrix{$lastDayOfMonth}{$user}{twoYearMark} =
834           PercentageOfMonth($twoYearMark{$user}{dateManipFormat},
835                   ParseDate(UnixDate(
836                   $twoYearMark{$user}{dateManipFormat}, "last day in %B %Y")));
837         $hoursToAdd = ($percent *
838                        ($THEREAFER_ACCURAL - $FIRST_TWO_YEAR_ACCURAL))
839           + $FIRST_TWO_YEAR_ACCURAL;
840       } elsif ($startDateYearMonth eq $yearMonth) {
841         $hoursToAdd =  PercentageOfMonth($START_DATES{$user}, ParseDate(
842                             UnixDate($START_DATES{$user}, "last day in %B %Y")))
843           * $FIRST_TWO_YEAR_ACCURAL;
844       } else {
845         $hoursToAdd = $FIRST_TWO_YEAR_ACCURAL;
846       }
847       $hoursToAdd = PartTimeCheckAndCalc($user, $lastDayOfMonth, $hoursToAdd);
848       $accruedMatrix{$lastDayOfMonth}{$user}{accruedThisMonth} = $hoursToAdd;
849       $totalsWithoutExpiration{$user} += $hoursToAdd;
850     }
851   }
852   my(%r) = _BuildDataStructure($db, $i_user, ParseDate('2000-02-01'),
853                                $i_endDate, 'ALL', 'ALL');
854   foreach my $topLevel(keys %r) {
855     foreach my $client (keys %{$r{$topLevel}}) {
856       foreach my $matter (keys %{$r{$topLevel}{$client}}) {
857         foreach my $user (keys %{$r{$topLevel}{$client}{$matter}}) {
858           foreach my $date (keys %{$r{$topLevel}{$client}{$matter}{$user}}) {
859             my $cat = "/$topLevel";
860             $cat .= "/$client" unless $topLevel eq $client;
861             $cat .= "/$matter";
862             next unless $cat =~
863               m%^(/other)?/admin/(?:vacation|personal|paid-time-off)%i;
864             my $lastDayOfMonth =ParseDate(UnixDate($date,"last day in %B %Y"));
865             $accruedMatrix{$lastDayOfMonth}{$user}{tookThisMonth} = 0.0
866               unless defined
867                 $accruedMatrix{$lastDayOfMonth}{$user}{tookThisMonth};
868             $accruedMatrix{$lastDayOfMonth}{$user}{tookThisMonth} +=
869               $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
870             $accruedMatrix{$lastDayOfMonth}{$user}{accruedThisMonth} -=
871               $r{$topLevel}{$client}{$matter}{$user}{$date}{hours};
872           }
873         }
874       }
875     }
876   }
877   my %userTotals;
878   foreach my $lastDayOfMonth (sort keys %accruedMatrix) {
879     foreach my $user (sort @users) {
880       $userTotals{$user}{totalBalance} = 0.0
881         unless defined $userTotals{$user}{totalBalance};
882       my $monthYear = UnixDate($lastDayOfMonth, "%Y-%m");
883       my $accrual = $accruedMatrix{$lastDayOfMonth}{$user}{accruedThisMonth};
884       $userTotals{$user}{$monthYear}{usedOrEarnedThisMonth} = $accrual;
885       $userTotals{$user}{totalBalance} += $accrual;
886       $userTotals{$user}{$monthYear}{tookThisMonth} =
887         $accruedMatrix{$lastDayOfMonth}{$user}{tookThisMonth};
888       if ($userTotals{$user}{totalBalance} > $MAXIMUM_BANKED and
889           UnixDate($lastDayOfMonth, "%B")
890           eq UnixDate($START_DATES{$user}, "%B")) {
891         $userTotals{$user}{$monthYear}{truncatedBy} = $MAXIMUM_BANKED -
892           $userTotals{$user}{totalBalance};
893         $userTotals{$user}{totalBalance} = $MAXIMUM_BANKED;
894       }
895     }
896   }
897   foreach my $user (sort keys %userTotals) {
898     print VAC_REPORT "User:          $user\n";
899     print VAC_REPORT "Start Date:    ", UnixDate($START_DATES{$user},
900                                                  "%Y-%m-%d"), "\n";
901     print VAC_REPORT "Time-Off Bank: ", sprintf("%.2f ",
902                                            $userTotals{$user}{totalBalance}),
903                      PL("hour", $userTotals{$user}{totalBalance});
904     print VAC_REPORT "\n\nDetails (reverse chronological order by month):\n";
905     my $startMonth = UnixDate($START_DATES{$user}, "%Y-%m");
906     foreach my $month (sort { $b cmp $a } keys %{$userTotals{$user}}) {
907       next if $month eq 'totalBalance' or $month lt $startMonth;
908       my $val = $userTotals{$user}{$month}{usedOrEarnedThisMonth};
909       print VAC_REPORT "     $month: ", sprintf("%8.2f ", abs($val)),
910                        PL("hour", $val),
911                        ($val <= 0.00) ? " subtracted from" : " added to",
912                          " your time-off bank\n";
913      if (defined $userTotals{$user}{$month}{tookThisMonth}) {
914         my $took = $userTotals{$user}{$month}{tookThisMonth};
915         print VAC_REPORT "             (", sprintf("%8.2f ",
916                                                             abs($took)),
917           PL("hour", $took), " taken)\n";
918       }
919       if (defined $userTotals{$user}{$month}{truncatedBy}) {
920         my $trunc = $userTotals{$user}{$month}{truncatedBy};
921         print VAC_REPORT "             (", sprintf("%8.2f ",
922                                                             abs($trunc)),
923           PL("hour", $trunc), " lost due to balance beyond $MAXIMUM_BANKED)\n";
924       }
925     }
926     print VAC_REPORT "\n\n", "=" x 77, "\n";
927   }
928 }
929
930
931 1;
932 __END__
933 # Local variables:
934 # compile-command: "perl -I ../../../Modules -c Reports.pm"
935 # End:
Note: See TracBrowser for help on using the browser.

SFLC Main Page

[frdm] Support SFLC