customer-specific account report (and some small refactoring of method names to clash...
authorivan <ivan>
Fri, 4 Dec 2009 04:45:44 +0000 (04:45 +0000)
committerivan <ivan>
Fri, 4 Dec 2009 04:45:44 +0000 (04:45 +0000)
18 files changed:
FS/FS/cust_bill.pm
FS/FS/cust_bill_event.pm
FS/FS/cust_event.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/svc_acct.pm
httemplate/misc/email-customers.html
httemplate/misc/process/bulk_change_pkg.cgi
httemplate/misc/process/email-customers.html
httemplate/search/477.html
httemplate/search/cust_bill.html
httemplate/search/cust_bill_event.cgi
httemplate/search/cust_event.html
httemplate/search/cust_main.html
httemplate/search/cust_pkg.cgi
httemplate/search/report_svc_acct.html
httemplate/search/svc_acct.cgi
httemplate/view/cust_main/packages.html

index af70add..cec7935 100644 (file)
@@ -4223,7 +4223,7 @@ sub re_X {
   my $distinct = '';
   my $orderby = 'ORDER BY cust_bill._date';
 
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
 
   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
@@ -4313,7 +4313,7 @@ sub credited_sql {
        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
 }
 
-=item search_sql HASHREF
+=item search_sql_where HASHREF
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
@@ -4356,10 +4356,10 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
 
 =cut
 
-sub search_sql {
+sub search_sql_where {
   my($class, $param) = @_;
   if ( $DEBUG ) {
-    warn "$me search_sql called with params: \n".
+    warn "$me search_sql_where called with params: \n".
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }
 
index 7c2ad37..36afed0 100644 (file)
@@ -201,7 +201,7 @@ sub retriable {
   $self->replace($old);
 }
 
-=item search_sql HASHREF
+=item search_sql_where HASHREF
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
@@ -236,7 +236,7 @@ Specifies the user for agent virtualization
 
 =cut
 
-sub search_sql {
+sub search_sql_where {
   my ($class, $params) = @_;
   my @search = ();
 
@@ -324,7 +324,7 @@ sub process_re_X {
 sub re_X {
   my($method, $param, $job) = @_;
 
-  my $where = FS::cust_bill_event->search_sql($param);
+  my $where = FS::cust_bill_event->search_sql_where($param);
   $where = " WHERE plan LIKE 'send%'". ( $where ? " AND $where" : "" );
 
   my $from = 'LEFT JOIN part_bill_event USING ( eventpart )'.
index bf3a5b7..d2fcfc1 100644 (file)
@@ -315,7 +315,7 @@ sub join_sql {
 
 }
 
-=item search_sql HASHREF
+=item search_sql_where HASHREF
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
@@ -347,10 +347,10 @@ specified in HASHREF.  Valid parameters are
 #Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
 #sub 
 
-sub search_sql {
+sub search_sql_where {
   my($class, $param) = @_;
   if ( $DEBUG ) {
-    warn "$me search_sql called with params: \n".
+    warn "$me search_sql_where called with params: \n".
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }
 
@@ -448,7 +448,7 @@ sub process_re_X {
 sub re_X {
   my($method, $param, $job) = @_;
 
-  my $search_sql = FS::cust_event->search_sql($param);
+  my $search_sql = FS::cust_event->search_sql_where($param);
 
   #maybe not...?  we do want the "re-" action to match the search more closely
   #            # yuck!  hardcoded *AND* sequential scans!
index 372aeb0..6e5bd10 100644 (file)
@@ -8153,7 +8153,7 @@ sub _money_table_where {
 
 }
 
-=item search_sql HASHREF
+=item search HASHREF
 
 (Class method)
 
@@ -8196,7 +8196,7 @@ bool
 
 =cut
 
-sub search_sql {
+sub search {
   my ($class, $params) = @_;
 
   my $dbh = dbh;
@@ -8396,13 +8396,13 @@ sub search_sql {
 
 }
 
-=item email_search_sql HASHREF
+=item email_search_result HASHREF
 
 (Class method)
 
 Emails a notice to the specified customers.
 
-Valid parameters are those of the L<search_sql> method, plus the following:
+Valid parameters are those of the L<search> method, plus the following:
 
 =over 4
 
@@ -8436,7 +8436,7 @@ retrying everything.
 
 =cut
 
-sub email_search_sql {
+sub email_search_result {
   my($class, $params) = @_;
 
   my $from = delete $params->{from};
@@ -8449,7 +8449,7 @@ sub email_search_sql {
   $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
     unless ref($params->{'payby'});
 
-  my $sql_query = $class->search_sql($params);
+  my $sql_query = $class->search($params);
 
   my $count_query   = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
@@ -8501,7 +8501,7 @@ sub email_search_sql {
 use Storable qw(thaw);
 use Data::Dumper;
 use MIME::Base64;
-sub process_email_search_sql {
+sub process_email_search_result {
   my $job = shift;
   #warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
@@ -8513,7 +8513,7 @@ sub process_email_search_sql {
   $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
     unless ref($param->{'payby'});
 
-  my $error = FS::cust_main->email_search_sql( $param );
+  my $error = FS::cust_main->email_search_result( $param );
   die $error if $error;
 
 }
@@ -8792,7 +8792,7 @@ sub smart_search {
     #getting complaints searches are not returning enough
     unless ( @cust_main  && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
 
-      #still some false laziness w/search_sql (was search/cust_main.cgi)
+      #still some false laziness w/search (was search/cust_main.cgi)
 
       #substring
 
index 58cca6d..c6d98fa 100644 (file)
@@ -2291,7 +2291,7 @@ sub cancel_sql {
   "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0";
 }
 
-=item search_sql HASHREF
+=item search HASHREF
 
 (Class method)
 
@@ -2364,7 +2364,7 @@ specifies the user for agent virtualization
 
 =cut
 
-sub search_sql { 
+sub search {
   my ($class, $params) = @_;
   my @where = ();
 
@@ -2593,10 +2593,10 @@ sub search_sql {
 
     if ($access_user) {
       push @where, $access_user->agentnums_sql('table'=>'cust_main');
-    }else{
+    } else {
       push @where, "1=0";
     }
-  }else{
+  } else {
     push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main');
   }
 
index 32dba25..bbf2c22 100644 (file)
@@ -28,6 +28,7 @@ use FS::Conf;
 use FS::Record qw( qsearch qsearchs fields dbh dbdef );
 use FS::Msgcat qw(gettext);
 use FS::UI::bytecount;
+use FS::UI::Web;
 use FS::part_pkg;
 use FS::svc_Common;
 use FS::cust_svc;
@@ -2500,6 +2501,144 @@ sub virtual_maildir {
 
 =back
 
+=head1 CLASS METHODS
+
+=over 4
+
+=item search HASHREF
+
+Class method which returns a qsearch hash expression to search for parameters
+specified in HASHREF.  Valid parameters are
+
+=over 4
+
+=item domain
+
+=item domsvc
+
+=item unlinked
+
+=item agentnum
+
+=item pkgpart
+
+Arrayref of pkgparts
+
+=item pkgpart
+
+=item where
+
+Arrayref of additional WHERE clauses, will be ANDed together.
+
+=item order_by
+
+=item cust_fields
+
+=back
+
+=cut
+
+sub search {
+  my ($class, $params) = @_;
+
+  my @where = ();
+
+  # domain
+  if ( $params->{'domain'} ) { 
+    my $svc_domain = qsearchs('svc_domain', { 'domain'=>$params->{'domain'} } );
+    #preserve previous behavior & bubble up an error if $svc_domain not found?
+    push @where, 'domsvc = '. $svc_domain->svcnum if $svc_domain;
+  }
+
+  # domsvc
+  if ( $params->{'domsvc'} =~ /^(\d+)$/ ) { 
+    push @where, "domsvc = $1";
+  }
+
+  #unlinked
+  push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
+
+  #agentnum
+  if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where, "agentnum = $1";
+  }
+
+  #custnum
+  if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
+    push @where, "custnum = $1";
+  }
+
+  #pkgpart
+  if ( $params->{'pkgpart'} && scalar(@{ $params->{'pkgpart'} }) ) {
+    #XXX untaint or sql quote
+    push @where,
+      'cust_pkg.pkgpart IN ('. join(',', @{ $params->{'pkgpart'} } ). ')';
+  }
+
+  # popnum
+  if ( $params->{'popnum'} =~ /^(\d+)$/ ) { 
+    push @where, "popnum = $1";
+  }
+
+  # svcpart
+  if ( $params->{'svcpart'} =~ /^(\d+)$/ ) { 
+    push @where, "svcpart = $1";
+  }
+
+
+  # here is the agent virtualization
+  #if ($params->{CurrentUser}) {
+  #  my $access_user =
+  #    qsearchs('access_user', { username => $params->{CurrentUser} });
+  #
+  #  if ($access_user) {
+  #    push @where, $access_user->agentnums_sql('table'=>'cust_main');
+  #  }else{
+  #    push @where, "1=0";
+  #  }
+  #} else {
+    push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
+                   'table'      => 'cust_main',
+                   'null_right' => 'View/link unlinked services',
+                 );
+  #}
+
+  push @where, @{ $params->{'where'} } if $params->{'where'};
+
+  my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
+
+  my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
+                  ' LEFT JOIN part_svc  USING ( svcpart ) '.
+                  ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
+                  ' LEFT JOIN cust_main USING ( custnum ) ';
+
+  my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
+  #if ( keys %svc_acct ) {
+  #  $count_query .= ' WHERE '.
+  #                    join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
+  #                                      keys %svc_acct
+  #                        );
+  #}
+
+  my $sql_query = {
+    'table'       => 'svc_acct',
+    'hashref'     => {}, # \%svc_acct,
+    'select'      => join(', ',
+                       'svc_acct.*',
+                       'part_svc.svc',
+                       'cust_main.custnum',
+                       FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
+                     ),
+    'addl_from'   => $addl_from,
+    'extra_sql'   => $extra_sql,
+    'order_by'    => $params->{'order_by'},
+    'count_query' => $count_query,
+  };
+
+}
+
+=back
+
 =head1 SUBROUTINES
 
 =over 4
index 46bf3b4..4e4c15f 100644 (file)
@@ -132,7 +132,7 @@ my $title = 'Bulk send customer notices';
 
 my $num_cust;
 if ( $cgi->param('magic') eq 'preview' ) {
-  my $sql_query = FS::cust_main->search_sql(\%search);
+  my $sql_query = FS::cust_main->search(\%search);
   my $count_query = delete($sql_query->{'count_query'});
   my $count_sth = dbh->prepare($count_query)
     or die "Error preparing $count_query: ". dbh->errstr;
index d2ab4bf..e22dafe 100755 (executable)
@@ -41,7 +41,7 @@ foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
 
 }
 
-my $sql_query = FS::cust_pkg->search_sql(\%search_hash);
+my $sql_query = FS::cust_pkg->search(\%search_hash);
 $sql_query->{'select'} = 'cust_pkg.pkgnum';
 
 my $error = FS::cust_pkg::bulk_change( [ $cgi->param('new_pkgpart') ],
index d254cfe..c54bc6d 100644 (file)
@@ -4,6 +4,6 @@
 die "access denied"
   unless $FS::CurrentUser::CurrentUser->access_right('Bulk send customer notices');
 
-my $server = new FS::UI::Web::JSRPC 'FS::cust_main::process_email_search_sql', $cgi; 
+my $server = new FS::UI::Web::JSRPC 'FS::cust_main::process_email_search_result', $cgi; 
 
 </%init>
index b485161..0f1502f 100755 (executable)
@@ -106,7 +106,7 @@ foreach my $row ( sort keys %row_option_name ) {
       if $column_option_name{$column};
     my $report_option = join(',', @report_option) if @report_option;
 
-    my $sql_query = FS::cust_pkg->search_sql(
+    my $sql_query = FS::cust_pkg->search(
       { %search_hash,
         ($report_option ? ( 'report_option' => $report_option ) : () ),
       }
index 4ca3e2f..c1f5517 100755 (executable)
@@ -121,7 +121,7 @@ if ( $cgi->param('invnum') =~ /^\s*(FS-)?(\d+)\s*$/ ) {
     $count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'";
   }
 
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql( \%search );
+  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where( \%search );
 
   unless ( $count_query ) {
     $count_query = 'SELECT COUNT(*), '. join(', ',
index ff4168d..16c9acd 100644 (file)
@@ -96,7 +96,7 @@ if ( $cgi->param('invnum') =~ /^(\d+)$/ ) {
   $search{invnum} = $1;
 }
 
-my $where = 'WHERE '. FS::cust_bill_event->search_sql( \%search );
+my $where = 'WHERE '. FS::cust_bill_event->search_sql_where( \%search );
 
 my $join = 'LEFT JOIN part_bill_event USING ( eventpart ) '.
            'LEFT JOIN cust_bill       USING ( invnum    ) '.
index b22644e..a0429e4 100644 (file)
@@ -173,7 +173,7 @@ my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi);
 $search{'beginning'} = $beginning;
 $search{'ending'}    = $ending;
 
-my $where = ' WHERE '. FS::cust_event->search_sql( \%search );
+my $where = ' WHERE '. FS::cust_event->search_sql_where( \%search );
 
 my $join = FS::cust_event->join_sql();
 
index 19eb131..829117b 100755 (executable)
@@ -85,7 +85,7 @@ $search_hash{'current_balance'} =
 # etc
 ###
 
-my $sql_query = FS::cust_main->search_sql(\%search_hash);
+my $sql_query = FS::cust_main->search(\%search_hash);
 my $count_query   = delete($sql_query->{'count_query'});
 my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
 my @extra_fields  = @{ delete($sql_query->{'extra_fields'})  };
index 37e8d6c..ff8ee57 100755 (executable)
@@ -204,7 +204,7 @@ foreach my $field (qw( setup last_bill bill adjourn susp expire cancel )) {
 
 }
 
-my $sql_query = FS::cust_pkg->search_sql(\%search_hash);
+my $sql_query = FS::cust_pkg->search(\%search_hash);
 my $count_query = delete($sql_query->{'count_query'});
 
 my $show = $curuser->default_customer_view =~ /^(jumbo|packages)$/
index 59fd1f8..ee913c4 100755 (executable)
@@ -1,7 +1,8 @@
-<% include('/elements/header.html', 'Account Report' ) %>
+<% include('/elements/header.html', $title ) %>
 
 <FORM ACTION="svc_acct.cgi" METHOD="GET">
 <INPUT TYPE="hidden" NAME="magic" VALUE="advanced">
+<INPUT TYPE="hidden" NAME="custnum" VALUE="<% $custnum %>">
 
   <TABLE BGCOLOR="#cccccc" CELLSPACING=0>
 
       <TH BGCOLOR="#e8e8e8" COLSPAN=2 ALIGN="left"><FONT SIZE="+1">Search options</FONT></TH>
     </TR>
 
+% unless ( $custnum ) {
     <% include( '/elements/tr-select-agent.html',
                    'curr_value'    => scalar( $cgi->param('agentnum') ),
                    'disable_empty' => 0,
               )
     %>
 
+%   # just this customer's domains?
     <% include( '/elements/tr-select-domain.html',
                    'element_name'  => 'domsvc',
                    'curr_value'    => scalar( $cgi->param('domsvc') ),
                    'disable_empty' => 0,
               )
     %>
-
+% }
 
     <SCRIPT type="text/javascript">
       function toggle(what) {
 <%init>
 
 die "access denied"
-  unless $FS::CurrentUser::CurrentUser->access_right('List packages');
+  unless $FS::CurrentUser::CurrentUser->access_right('List packages'); #?
+
+my $title = 'Account Report';
+
+#false laziness w/report_cust_pkg.html
+my $custnum = '';
+if ( $cgi->param('custnum') =~ /^(\d+)$/ ) {
+  $custnum = $1;
+  my $cust_main = qsearchs({
+    'table'     => 'cust_main', 
+    'hashref'   => { 'custnum' => $custnum },
+    'extra_sql' => ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql,
+  }) or die "unknown custnum $custnum";
+  $title .= ': '. $cust_main->name;
+}
 
 </%init>
 <%once>
index 7ddb3f0..e2abf56 100755 (executable)
@@ -64,6 +64,7 @@ my $link_cust = sub {
   }
 };
 
+my %search_hash = ();
 my @extra_sql = ();
 
 my @header = ( '#', 'Service', 'Account', 'UID', 'Last Login' );
@@ -73,19 +74,8 @@ my $align = 'rlllr';
 my @color = ( '', '', '', '', '' );
 my @style = ( '', '', '', '', '' );
 
-if ( $cgi->param('domain') ) { 
-  my $svc_domain =
-    qsearchs('svc_domain', { 'domain' => $cgi->param('domain') } );
-  unless ( $svc_domain ) {
-    #it would be nice if this looked more like the other "not found"
-    #errors, but this will do for now.
-    errorpage("Domain ". $cgi->param('domain'). " not found at all");
-  } else {
-    push @extra_sql, 'domsvc = '. $svc_domain->svcnum;
-  }
-}
-if ( $cgi->param('domsvc') =~ /^(\d+)$/ ) { 
-  push @extra_sql, "domsvc = $1";
+for (qw( domain domsvc agentnum custnum popnum svcpart cust_fields )) {
+  $search_hash{$_} = $cgi->param($_) if length($cgi->param($_));
 }
 
 my $timepermonth = '';
@@ -93,7 +83,7 @@ my $timepermonth = '';
 my $orderby = 'ORDER BY svcnum';
 if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 
-  push @extra_sql, 'pkgnum IS NULL'
+  $search_hash{'unlinked'} = 1
     if $cgi->param('magic') eq 'unlinked';
 
   my $sortby = '';
@@ -101,7 +91,7 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
     $sortby = $1;
     $sortby = "LOWER($sortby)"
       if $sortby eq 'username';
-    push @extra_sql, "$sortby IS NOT NULL"
+    push @extra_sql, "$sortby IS NOT NULL" #XXX search_hash
       if $sortby eq 'uid' || $sortby eq 'seconds' || $sortby eq 'last_login';
     $orderby = "ORDER BY $sortby";
   }
@@ -166,16 +156,11 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
   }
 
 } elsif ( $cgi->param('magic') =~ /^advanced$/ ) {
+
   $orderby = "";
 
-  if ( $cgi->param('agentnum') =~ /^(\d+)$/ and $1 ) {
-    push @extra_sql, "agentnum = $1";
-  }
+  $search_hash{'pkgpart'} = [ $cgi->param('pkgpart') ];
 
-  my $pkgpart = join (' OR cust_pkg.pkgpart=',
-                      grep {$_} map { /^(\d+)$/; } ($cgi->param('pkgpart')));
-  push @extra_sql,  '(cust_pkg.pkgpart=' . $pkgpart . ')' if $pkgpart;
-                      
   foreach my $field (qw( last_login last_logout )) {
 
     my($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, $field);
@@ -201,11 +186,9 @@ if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 
   $orderby ||= "ORDER BY svcnum";
 
-} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
-  push @extra_sql, "popnum = $1";
+} elsif ( $cgi->param('popnum') ) {
   $orderby = "ORDER BY LOWER(username)";
-} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
-  push @extra_sql, "svcpart = $1";
+} elsif ( $cgi->param('svcpart') ) {
   $orderby = "ORDER BY uid";
   #$orderby = "ORDER BY svcnum";
 } else {
@@ -260,40 +243,10 @@ $align .= FS::UI::Web::cust_aligns();
 push @color, FS::UI::Web::cust_colors();
 push @style, FS::UI::Web::cust_styles();
 
-my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
-                ' LEFT JOIN part_svc  USING ( svcpart ) '.
-                ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
-                ' LEFT JOIN cust_main USING ( custnum ) ';
-
-#here is the agent virtualization
-push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql( 
-                   'null_right' => 'View/link unlinked services'
-                 );
-
-my $extra_sql = 
-  scalar(@extra_sql)
-    ? ' WHERE '. join(' AND ', @extra_sql )
-    : '';
-
-my $count_query = "SELECT COUNT(*) FROM svc_acct $addl_from $extra_sql";
-#if ( keys %svc_acct ) {
-#  $count_query .= ' WHERE '.
-#                    join(' AND ', map "$_ = ". dbh->quote($svc_acct{$_}),
-#                                      keys %svc_acct
-#                        );
-#}
-
-my $sql_query = {
-  'table' => 'svc_acct',
-  'hashref'   => {}, # \%svc_acct,
-  'select'    => join(', ',
-                    'svc_acct.*',
-                    'part_svc.svc',
-                    'cust_main.custnum',
-                    FS::UI::Web::cust_sql_fields(),
-                  ),
-  'extra_sql' => "$extra_sql $orderby",
-  'addl_from' => $addl_from,
-};
+$search_hash{'order_by'} = $orderby;
+$search_hash{'where'} = \@extra_sql;
+
+my $sql_query = FS::svc_acct->search(\%search_hash);
+my $count_query = delete($sql_query->{'count_query'});
 
 </%init>
index dbbdf67..bd056a3 100755 (executable)
@@ -65,7 +65,9 @@ Current packages
 
     </TD>
     <TD ALIGN="right">
-      <A HREF="<%$p%>search/report_cust_pkg.html?custnum=<% $cust_main->custnum %>">Package reports</A>
+      <A HREF="<%$p%>search/report_cust_pkg.html?custnum=<% $cust_main->custnum %>">Package reports</A><BR>
+      Service reports:
+        <A HREF="<%$p%>search/report_svc_acct.html?custnum=<% $cust_main->custnum %>">accounts</A>
     </TD>
   </TR>