adding a basic change history using history tables, RT#1005, RT#4357
authorivan <ivan>
Tue, 28 Jul 2009 21:17:45 +0000 (21:17 +0000)
committerivan <ivan>
Tue, 28 Jul 2009 21:17:45 +0000 (21:17 +0000)
FS/FS/AccessRight.pm
FS/FS/Conf.pm
FS/FS/Mason.pm
FS/FS/svc_external.pm
httemplate/pref/pref.html
httemplate/view/cust_main.cgi
httemplate/view/cust_main/change_history.html [new file with mode: 0644]

index 3157d5f..29cecd5 100644 (file)
@@ -94,6 +94,7 @@ tie my %rights, 'Tie::IxHash',
     'View customer',
     #'View Customer | View tickets',
     'Edit customer',
+    'View customer history',
     'Cancel customer',
     'Complimentary customer', #aka users-allow_comp 
     { rightname=>'Delete customer', desc=>"Enable customer deletions. Be very careful! Deleting a customer will remove all traces that this customer ever existed! It should probably only be used when auditing a legacy database. Normally, you cancel all of a customer's packages if they cancel service." }, #aka. deletecustomers
index 0fe3ca9..20abd45 100644 (file)
@@ -2319,6 +2319,13 @@ worry that config_items is freeside-specific and icky.
   },
 
   {
+    'key'         => 'change_history-years',
+    'section'     => 'UI',
+    'description' => 'Number of years of change history to show by default.  Currently defaults to 0.5.',
+    'type'        => 'text',
+  },
+
+  {
     'key'         => 'cust_main-packages-years',
     'section'     => 'UI',
     'description' => 'Number of years to show old (cancelled and one-time charge) packages by default.  Currently defaults to 2.',
@@ -2976,7 +2983,7 @@ worry that config_items is freeside-specific and icky.
       'tickets'         => 'Tickets',
       'packages'        => 'Packages',
       'payment_history' => 'Payment History',
-      #''                => 'Change History',
+      'change_history'  => 'Change History',
       'jumbo'           => 'Jumbo',
     ],
   },
index ac11026..ed99bf6 100644 (file)
@@ -186,6 +186,16 @@ Initializes the Mason environment, loads all Freeside and RT libraries, etc.
   use FS::part_pkg_taxrate;
   use FS::tax_rate;
   use FS::part_pkg_report_option;
+  use FS::h_cust_pkg;
+  use FS::h_svc_acct;
+  use FS::h_svc_broadband;
+  use FS::h_svc_domain;
+  #use FS::h_domain_record;
+  use FS::h_svc_external;
+  use FS::h_svc_forward;
+  use FS::h_svc_phone;
+  #use FS::h_phone_device;
+  use FS::h_svc_www;
   # Sammath Naur
 
   if ( %%%RT_ENABLED%%% ) {
@@ -223,7 +233,7 @@ Initializes the Mason environment, loads all Freeside and RT libraries, etc.
 
       #slow, unreliable, segfaults and is optional
       #see rt/html/Ticket/Elements/ShowTransactionAttachments
-      #use Text::Quoted;
+      use Text::Quoted;
 
       #?#use File::Path qw( rmtree );
       #?#use File::Glob qw( bsd_glob );
index 0fb391f..aca7c1b 100644 (file)
@@ -95,6 +95,7 @@ sub label {
       substr('0000000000'.uc($self->title), -10);
   } else {
     #$self->SUPER::label;
+    return $self->id unless $self->title =~ /\S/;
     $self->id. ' - '. $self->title;
   }
 }
index 8bdf6c0..562ef29 100644 (file)
@@ -124,25 +124,23 @@ Vonage integration (see <a href="https://secure.click2callu.com/">Click2Call</a>
 <INPUT TYPE="submit" VALUE="Update preferences">
 
 <% include('/elements/footer.html') %>
-<%once>
-
-  #false laziness w/view/cust_main.cgi and Conf.pm (cust_main-default_view)
-
-  tie my %customer_views, 'Tie::IxHash',
-    'Basics'          => 'basics',
-    'Notes'           => 'notes', #notes and files?
-    'Tickets'         => 'tickets',
-    'Packages'        => 'packages',
-    'Payment History' => 'payment_history',
-    #'Change History'  => '',
-    'Jumbo'           => 'jumbo',
-  ;
-
-</%once>
 <%init>
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
+#false laziness w/view/cust_main.cgi and Conf.pm (cust_main-default_view)
+
+tie my %customer_views, 'Tie::IxHash',
+  'Basics'          => 'basics',
+  'Notes'           => 'notes', #notes and files?
+  'Tickets'         => 'tickets',
+  'Packages'        => 'packages',
+  'Payment History' => 'payment_history',
+;
+$customer_views{'Change History'} = 'change_history'
+  if $curuser->access_right('View customer history');
+$customer_views{'Jumbo'} = 'jumbo';
+
 # XSS via your own preferences?  seems unlikely, but nice try anyway...
 ( $curuser->option('menu_position') || 'top' )
   =~ /^(\w+)$/ or die "illegal menu_position";
index 88fd037..78bcb1f 100755 (executable)
@@ -113,6 +113,7 @@ Comments
 % if ( ! $conf->exists('cust_main-disable_notes') || $notecount) {
 
 %   unless ( $view eq 'notes' && $cust_main->comments !~ /[^\s\n\r]/ ) {
+      <BR>
       <A NAME="cust_main_note"><FONT SIZE="+2">Notes</FONT></A><BR>
 %   }
 
@@ -180,6 +181,10 @@ Comments
 
 % }
 
+% if ( $view eq 'change_history' ) { #  || $view eq 'jumbo'
+  <% include('cust_main/change_history.html', $cust_main ) %>
+% }
+
 <% include('/elements/footer.html') %>
 <%init>
 
@@ -213,11 +218,12 @@ tie my %views, 'Tie::IxHash',
        'Notes'            => 'notes', #notes and files?
 ;
 $views{'Tickets'}         =  'tickets'
-                               if $conf->config('ticket_system');
+  if $conf->config('ticket_system');
 $views{'Packages'}        =  'packages';
 $views{'Payment History'} =  'payment_history'
-                               unless $conf->config('payby-default' eq 'HIDE');
-#$views{'Change History'}  =  '';
+  unless $conf->config('payby-default' eq 'HIDE');
+$views{'Change History'}  =  'change_history'
+  if $curuser->access_right('View customer history');
 $views{'Jumbo'}           =  'jumbo';
 
 my %viewname = reverse %views;
diff --git a/httemplate/view/cust_main/change_history.html b/httemplate/view/cust_main/change_history.html
new file mode 100644 (file)
index 0000000..1700bc3
--- /dev/null
@@ -0,0 +1,302 @@
+% if ( int( time - (keys %years)[0] * 31556736 ) > $start ) {
+    Show:
+%   my $chy = $cgi->param('change_history-years');
+%   foreach my $y (keys %years) {
+%     if ( $y == $years ) {
+        <FONT SIZE="+1"><% $years{$y} %></FONT>
+%     } else {
+%       $cgi->param('change_history-years', $y);
+        <A HREF="<% $cgi->self_url %>"><% $years{$y} %></A>
+%     }
+%     last if int( time - $y * 31556736 ) < $start;
+%   }
+%   $cgi->param('change_history-years', $chy);
+% }
+
+<% include("/elements/table-grid.html") %>
+% my $bgcolor1 = '#eeeeee';
+%   my $bgcolor2 = '#ffffff';
+%   my $bgcolor = '';
+
+<TR>
+  <TH CLASS="grid" BGCOLOR="#cccccc">User</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Date</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Time</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Item</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Action</TH>
+  <TH CLASS="grid" BGCOLOR="#cccccc">Description</TH>
+</TR>
+
+% foreach my $item ( sort { $a->history_date <=> $b->history_date
+%                           #|| table order
+%                           || $a->historynum <=> $b->historynum
+%                         }
+%                         @history
+%                  )
+% {
+%
+%   my $history_other = '';
+%   my $act  = $item->history_action;
+%   if ( $act =~ /^replace/ ) {
+%     my $pkey = $item->primary_key;
+%     my $date = $item->history_date;
+%     $history_other = qsearchs({
+%       'table'     => $item->table,
+%       'hashref'   => { $pkey            => $item->$pkey(),
+%                        'history_action' => $replace_other{$act},
+%                        'historynum'     => { 'op'    => $replace_dir{$act},
+%                                              'value' => $item->historynum
+%                                            },
+%                      },
+%       'extra_sql' => "
+%         AND history_date $replace_direq{$act} $date
+%         AND ($date $replace_op{$act} $fuzz) $replace_direq{$act} history_date
+%         ORDER BY historynum $replace_ord{$act} LIMIT 1
+%       ",
+%     });
+%   }
+%
+%   if ( $bgcolor eq $bgcolor1 ) {
+%     $bgcolor = $bgcolor2;
+%   } else {
+%     $bgcolor = $bgcolor1;
+%   }
+
+  <TR>
+    <TD ALIGN="left" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+%     my $otaker = $item->history_user;
+%     $otaker = '<i>auto billing</i>'          if $otaker eq 'fs_daily';
+%     $otaker = '<i>customer self-service</i>' if $otaker eq 'fs_selfservice';
+%     $otaker = '<i>job queue</i>'             if $otaker eq 'fs_queue';
+      <% $otaker %>
+    </TD>
+    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+%     my $d = time2str('%b %o, %Y', $item->history_date );
+%     $d =~ s/ /&nbsp;/g;
+      <% $d %>
+    </TD>
+    <TD ALIGN="right" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+%     my $t = time2str('%r', $item->history_date );
+%     $t =~ s/ /&nbsp;/g;
+      <% $t %>
+    </TD>
+    <TD ALIGN="center" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+%     my $label = $h_tables{$item->table};
+%     $label = &{ $h_table_labelsub{$item->table} }( $item, $label )
+%       if $h_table_labelsub{$item->table};
+      <% $label %>
+    </TD>
+    <TD ALIGN="left" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% $action{$item->history_action} %>
+    </TD>
+    <TD ALIGN="left" CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <% join(', ',
+           map  { my $value = ( $_ =~ /(^pay(info|cvv)|^ss|_password)$/ ) 
+                                ? 'N/A'
+                                : $item->get($_);
+                  $value = substr($value, 0, 77).'...' if length($value) > 80;
+                  $value = encode_entities($value);
+                  "<I>$_</I>:<B>$value</B>";
+                }
+           grep { $history_other
+                    ? ( $item->get($_) ne $history_other->get($_) )
+                    : ( $item->get($_) =~ /\S/ )
+                }
+           grep { ! /^(history|custnum$)/i }
+                $item->fields
+         )
+      %>
+    </TD>
+  </TR>
+
+% }
+
+</TABLE>
+<%once>
+
+# length-switching 
+
+tie my %years, 'Tie::IxHash',
+    .5 => '6 months',
+   1  => '1 year',
+   2  => '2 years',
+   5  => '5 years',
+  39  => 'all history',
+;
+
+# labeling history rows
+
+my %action = (
+  'insert'      => 'Insert', #'Create',
+  'replace_old' => 'Change&nbsp;from',
+  'replace_new' => 'Change&nbsp;to',
+  'delete'      => 'Remove',
+);
+
+# finding the other replace row
+
+my %replace_other = (
+  'replace_new' => 'replace_old',
+  'replace_old' => 'replace_new',
+);
+my %replace_dir = (
+  'replace_new' => '<',
+  'replace_old' => '>',
+);
+my %replace_direq = (
+  'replace_new' => '<=',
+  'replace_old' => '>=',
+);
+my %replace_op = (
+  'replace_new' => '-',
+  'replace_old' => '+',
+);
+my %replace_ord = (
+  'replace_new' => 'DESC',
+  'replace_old' => 'ASC',
+);
+
+my $fuzz = 5; #seems like a lot
+
+# which tables to search and what to call them
+
+tie my %tables, 'Tie::IxHash',
+  'cust_main'         => 'Customer',
+  'cust_main_invoice' => 'Invoice destination',
+  'cust_pkg'          => 'Package',
+  #? or just svc_* ? 'cust_svc' => 
+  'svc_acct'          => 'Account',
+  'radius_usergroup'  => 'RADIUS group',
+  'svc_domain'        => 'Domain',
+  'svc_www'           => 'Hosting',
+  'svc_forward'       => 'Mail forward',
+  'svc_broadband'     => 'Broadband',
+  'svc_external'      => 'External service',
+  'svc_phone'         => 'Phone',
+  'phone_device'      => 'Phone device',
+  #? it gets provisioned anyway 'phone_avail'         => 'Phone',
+;
+
+my $svc_join = 'JOIN cust_svc USING ( svcnum ) JOIN cust_pkg USING ( pkgnum )';
+
+my %table_join = (
+  'svc_acct'         => $svc_join,
+  'radius_usergroup' => $svc_join,
+  'svc_domain'       => $svc_join,
+  'svc_www'          => $svc_join,
+  'svc_forward'      => $svc_join,
+  'svc_broadband'    => $svc_join,
+  'svc_external'     => $svc_join,
+  'svc_phone'        => $svc_join,
+  'phone_device'     => $svc_join,
+);
+
+my %h_tables = map { ( "h_$_" => $tables{$_} ) } keys %tables;
+
+my %pkgpart = ();
+my $pkg_labelsub = sub {
+  my($item, $label) = @_;
+  $pkgpart{$item->pkgpart} ||= $item->part_pkg->pkg;
+  $label. ': <b>'. encode_entities($pkgpart{$item->pkgpart}). '</b>';
+};
+
+my $svc_labelsub = sub {
+  my($item, $label) = @_;
+  $label. ': <b>'. encode_entities($item->label). '</b>';
+};
+
+my %h_table_labelsub = (
+  'h_cust_pkg'      => $pkg_labelsub,
+  'h_svc_acct'      => $svc_labelsub,
+  #'h_radius_usergroup' =>
+  'h_svc_domain'    => $svc_labelsub,
+  'h_svc_www'       => $svc_labelsub,
+  'h_svc_forward'   => $svc_labelsub,
+  'h_svc_broadband' => $svc_labelsub,
+  'h_svc_external'  => $svc_labelsub,
+  'h_svc_phone'     => $svc_labelsub,
+  #'h_phone_device'
+);
+
+# cust_main
+# cust_main_invoice
+
+# cust_pkg
+# cust_pkg_option?
+# cust_pkg_detail
+# cust_pkg_reason?  no
+
+#cust_svc
+#cust_svc_option?
+#svc_*
+# svc_acct
+#  radius_usergroup
+#  acct_snarf?  is this even used?
+# svc_domain
+#  domain_record
+#  registrar
+# svc_forward
+# svc_www
+# svc_broadband
+#  (virtual fields?  eh... maybe when they're real)
+# svc_external
+# svc_phone
+#  phone_device
+#  phone_avail
+
+# future:
+
+# inventory_item (from services)
+# pkg_referral? (changed?)
+
+#random others:
+
+# cust_location?
+# cust_main-exemption?? (295.ca named tax exemptions)
+
+</%once>
+<%init>
+
+my( $cust_main ) = @_;
+
+my $conf = new FS::Conf;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access deined"
+  unless $curuser->access_right('View customer history');
+
+# find out the beginning of this customer history, if possible
+my $h_insert = qsearchs({
+  'table'     => 'h_cust_main',
+  'hashref'   => { 'custnum'        => $cust_main->custnum,
+                   'history_action' => 'insert',
+                 },
+  'extra_sql' => 'ORDER BY historynum LIMIT 1',
+});
+my $start = $h_insert ? $h_insert->history_date : 0;
+
+# retreive the history
+
+my @history = ();
+
+my $years = $conf->config('change_history-years') || .5;
+if ( $cgi->param('change_history-years') =~ /^([\d\.]+)$/ ) {
+  $years = $1;
+}
+my $newer_than = int( time - $years * 31556736 ); #60*60*24*365.24
+
+local($FS::Record::nowarn_classload) = 1;
+
+foreach my $table ( keys %tables ) {
+  my @items = qsearch({
+    'table'     => "h_$table",
+    'addl_from' => $table_join{$table},
+    'hashref'   => { 'history_date' =>  { op=>'>=', value=>$newer_than }, },
+    'extra_sql' => ' AND custnum = '. $cust_main->custnum,
+  });
+  push @history, @items;
+
+}
+
+</%init>