new 'jsearch' call for big joined searches & caching support
authorivan <ivan>
Sat, 3 Nov 2001 17:49:52 +0000 (17:49 +0000)
committerivan <ivan>
Sat, 3 Nov 2001 17:49:52 +0000 (17:49 +0000)
preliminary customer browse optimizations, much faster!

FS/FS/Record.pm
FS/FS/SearchCache.pm [new file with mode: 0644]
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/svc_acct.pm
FS/MANIFEST
FS/t/SearchCache.t [new file with mode: 0644]
httemplate/search/cust_main.cgi

index ec32645..3c8e9ba 100644 (file)
@@ -9,9 +9,10 @@ use File::CounterFile;
 use Locale::Country;
 use DBIx::DBSchema 0.19;
 use FS::UID qw(dbh checkruid getotaker datasrc driver_name);
+use FS::SearchCache;
 
 @ISA = qw(Exporter);
-@EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef);
+@EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch);
 
 $DEBUG = 0;
 
@@ -135,9 +136,31 @@ sub new {
     }
   }
 
+  $self->_cache($hashref, shift) if $self->can('_cache') && @_;
+
   $self;
 }
 
+sub new_or_cached {
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my $self = {};
+  bless ($self, $class);
+
+  $self->{'Table'} = shift unless defined ( $self->table );
+
+  my $hashref = $self->{'Hash'} = shift;
+  my $cache = shift;
+  if ( defined( $cache->cache->{$hashref->{$cache->key}} ) ) {
+    my $obj = $cache->cache->{$hashref->{$cache->key}};
+    $obj->_cache($hashref, $cache) if $obj->can('_cache');
+    $obj;
+  } else {
+    $cache->cache->{$hashref->{$cache->key}} = $self->new($hashref, $cache);
+  }
+
+}
+
 sub create {
   my $proto = shift;
   my $class = ref($proto) || $proto;
@@ -170,15 +193,19 @@ objects.
 =cut
 
 sub qsearch {
-  my($table, $record, $select, $extra_sql ) = @_;
-  $table =~ /^([\w\_]+)$/ or die "Illegal table: $table";
-  $table = $1;
+  my($stable, $record, $select, $extra_sql, $cache ) = @_;
+  #$stable =~ /^([\w\_]+)$/ or die "Illegal table: $table";
+  #for jsearch
+  $stable =~ /^([\w\s\(\)\.\,\=]+)$/ or die "Illegal table: $stable";
+  $stable = $1;
   $select ||= '*';
   my $dbh = dbh;
 
+  my $table = $cache ? $cache->table : $stable;
+
   my @fields = grep exists($record->{$_}), fields($table);
 
-  my $statement = "SELECT $select FROM $table";
+  my $statement = "SELECT $select FROM $stable";
   if ( @fields ) {
     $statement .= ' WHERE '. join(' AND ', map {
       if ( ! defined( $record->{$_} ) || $record->{$_} eq '' ) {
@@ -206,9 +233,15 @@ sub qsearch {
   if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
     if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
       #derivied class didn't override new method, so this optimization is safe
-      map {
-        new( "FS::$table", { %{$_} } )
-      } @{$sth->fetchall_arrayref( {} )};
+      if ( $cache ) {
+        map {
+          new_or_cached( "FS::$table", { %{$_} }, $cache )
+        } @{$sth->fetchall_arrayref( {} )};
+      } else {
+        map {
+          new( "FS::$table", { %{$_} } )
+        } @{$sth->fetchall_arrayref( {} )};
+      }
     } else {
       warn "untested code (class FS::$table uses custom new method)";
       map {
@@ -224,6 +257,25 @@ sub qsearch {
 
 }
 
+=item jsearch
+
+Experimental JOINed search method.  Using this method, you can execute a
+single SELECT spanning multiple tables, and cache the results for subsequent
+method calls.  Interface will almost definately change in an incompatible
+fashion.
+
+=cut
+
+sub jsearch {
+  my($table, $record, $select, $extra_sql, $ptable, $pkey ) = @_;
+  my $cache = FS::SearchCache->new( $ptable, $pkey );
+  my %saw;
+  ( $cache,
+    grep { !$saw{$_->getfield($pkey)}++ }
+      qsearch($table, $record, $select, $extra_sql, $cache )
+  );
+}
+
 =item qsearchs TABLE, HASHREF
 
 Same as qsearch, except that if more than one record matches, it B<carp>s but
@@ -312,16 +364,30 @@ $record->column('value') is a synonym for $record->set('column','value');
 
 =cut
 
+# readable/safe
+#sub AUTOLOAD {
+#  my($self,$value)=@_;
+#  my($field)=$AUTOLOAD;
+#  $field =~ s/.*://;
+#  if ( defined($value) ) {
+#    confess "errant AUTOLOAD $field for $self (arg $value)"
+#      unless $self->can('setfield');
+#    $self->setfield($field,$value);
+#  } else {
+#    confess "errant AUTOLOAD $field for $self (no args)"
+#      unless $self->can('getfield');
+#    $self->getfield($field);
+#  }    
+#}
+
+# efficient
 sub AUTOLOAD {
-  my($self,$value)=@_;
-  my($field)=$AUTOLOAD;
+  my $field = $AUTOLOAD;
   $field =~ s/.*://;
-  if ( defined($value) ) {
-    confess "errant AUTOLOAD $field for $self (arg $value)"
-      unless $self->can('setfield');
-    $self->setfield($field,$value);
+  if ( scalar(@_) == 2 ) {
+    $_[0]->setfield($field, $_[1]);
   } else {
-    $self->getfield($field);
+    $_[0]->getfield($field);
   }    
 }
 
@@ -992,10 +1058,6 @@ sub DESTROY { return; }
 
 =back
 
-=head1 VERSION
-
-$Id: Record.pm,v 1.31 2001-11-02 05:11:52 ivan Exp $
-
 =head1 BUGS
 
 This module should probably be renamed, since much of the functionality is
diff --git a/FS/FS/SearchCache.pm b/FS/FS/SearchCache.pm
new file mode 100644 (file)
index 0000000..4218acf
--- /dev/null
@@ -0,0 +1,96 @@
+package FS::SearchCache;
+
+use strict;
+use vars qw($DEBUG);
+#use Carp qw(carp cluck croak confess);
+
+$DEBUG = 0;
+
+=head1 NAME
+
+FS::SearchCache - cache
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+=head1 METHODS
+
+=over 4
+
+=item new
+
+=cut
+
+sub new { 
+  my $proto = shift;
+  my $class = ref($proto) || $proto;
+  my( $table, $key ) = @_;
+  warn "table $table\n" if $DEBUG > 1;
+  warn "key $key\n" if $DEBUG > 1;
+  my $self = { 'table' => $table,
+               'key'   => $key,
+               'cache' => {},
+               'subcache' => {},
+             };
+  bless ($self, $class);
+
+  $self;
+}
+
+=item table
+
+=cut
+
+sub table { my $self = shift; $self->{table}; }
+
+=item key
+
+=cut
+
+sub key { my $self = shift; $self->{key}; }
+
+=item cache
+
+=cut
+
+sub cache { my $self = shift; $self->{cache}; }
+
+=item subcache
+
+=cut
+
+sub subcache {
+  my $self = shift;
+  my $col = shift;
+  my $table = shift;
+  my $keyval = shift;
+  if ( exists $self->{subcache}->{$col}->{$keyval} ) {
+    warn "returning existing subcache for $keyval ($col)".
+         "$self->{subcache}->{$col}->{$keyval}\n" if $DEBUG;
+    return $self->{subcache}->{$col}->{$keyval};
+  } else {
+    #my $tablekey = @_ ? shift : $col;
+    my $tablekey = $col;
+    my $subcache = ref($self)->new( $table, $tablekey );
+    $self->{subcache}->{$col}->{$keyval} = $subcache;
+    warn "creating new subcache $table $tablekey: $subcache\n" if $DEBUG;
+    $subcache;
+  }
+}
+
+=back
+
+=head1 BUGS
+
+Dismal documentation.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::cust_main>
+
+=cut
+
+1;
+
+
index dfb7125..4c4fe87 100644 (file)
@@ -75,6 +75,18 @@ $FS::UID::callback{'FS::cust_main'} = sub {
   }
 };
 
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( exists $hashref->{'pkgnum'} ) {
+#    #@{ $self->{'_pkgnum'} } = ();
+    my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
+    $self->{'_pkgnum'} = $subcache;
+    #push @{ $self->{'_pkgnum'} },
+    FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
+  }
+}
+
 =head1 NAME
 
 FS::cust_main - Object methods for cust_main records
@@ -701,7 +713,11 @@ Returns all packages (see L<FS::cust_pkg>) for this customer.
 
 sub all_pkgs {
   my $self = shift;
-  qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+  if ( $self->{'_pkgnum'} ) {
+    values %{ $self->{'_pkgnum'}->cache };
+  } else {
+    qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+  }
 }
 
 =item ncancelled_pkgs
@@ -712,16 +728,20 @@ Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
 
 sub ncancelled_pkgs {
   my $self = shift;
-  @{ [ # force list context
-    qsearch( 'cust_pkg', {
-      'custnum' => $self->custnum,
-      'cancel'  => '',
-    }),
-    qsearch( 'cust_pkg', {
-      'custnum' => $self->custnum,
-      'cancel'  => 0,
-    }),
-  ] };
+  if ( $self->{'_pkgnum'} ) {
+    grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
+  } else {
+    @{ [ # force list context
+      qsearch( 'cust_pkg', {
+        'custnum' => $self->custnum,
+        'cancel'  => '',
+      }),
+      qsearch( 'cust_pkg', {
+        'custnum' => $self->custnum,
+        'cancel'  => 0,
+      }),
+    ] };
+  }
 }
 
 =item suspended_pkgs
@@ -1855,7 +1875,7 @@ sub append_fuzzyfiles {
 
 =head1 VERSION
 
-$Id: cust_main.pm,v 1.44 2001-10-22 08:31:25 ivan Exp $
+$Id: cust_main.pm,v 1.45 2001-11-03 17:49:52 ivan Exp $
 
 =head1 BUGS
 
index 7aee8d0..19e1da3 100644 (file)
@@ -20,6 +20,27 @@ use FS::svc_www;
 
 @ISA = qw( FS::Record );
 
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  #if ( $hashref->{'pkgpart'} ) {
+  if ( $hashref->{'pkg'} ) {
+    # #@{ $self->{'_pkgnum'} } = ();
+    # my $subcache = $cache->subcache('pkgpart', 'part_pkg');
+    # $self->{'_pkgpart'} = $subcache;
+    # #push @{ $self->{'_pkgnum'} },
+    #   FS::part_pkg->new_or_cached($hashref, $subcache);
+    $self->{'_pkgpart'} = FS::part_pkg->new($hashref);
+  }
+  if ( exists $hashref->{'svcnum'} ) {
+    #@{ $self->{'_pkgnum'} } = ();
+    my $subcache = $cache->subcache('svcnum', 'cust_svc', $hashref->{pkgnum});
+    $self->{'_svcnum'} = $subcache;
+    #push @{ $self->{'_pkgnum'} },
+    FS::cust_svc->new_or_cached($hashref, $subcache) if $hashref->{svcnum};
+  }
+}
+
 =head1 NAME
 
 FS::cust_pkg - Object methods for cust_pkg objects
@@ -420,7 +441,26 @@ L<FS::part_pkg>).
 
 sub part_pkg {
   my $self = shift;
-  qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+  #exists( $self->{'_pkgpart'} )
+  $self->{'_pkgpart'}
+    ? $self->{'_pkgpart'}
+    : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
+}
+
+=item cust_svc
+
+Returns the services for this package, as FS::cust_svc objects (see
+L<FS::cust_svc>)
+
+=cut
+
+sub cust_svc {
+  my $self = shift;
+  if ( $self->{'_svcnum'} ) {
+    values %{ $self->{'_svcnum'}->cache };
+  } else {
+    qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
+  }
 }
 
 =item labels
@@ -432,7 +472,7 @@ Returns a list of lists, calling the label method for all services
 
 sub labels {
   my $self = shift;
-  map { [ $_->label ] } qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } );
+  map { [ $_->label ] } $self->cust_svc;
 }
 
 =item cust_main
@@ -589,7 +629,7 @@ sub order {
 
 =head1 VERSION
 
-$Id: cust_pkg.pm,v 1.12 2001-10-22 08:29:42 ivan Exp $
+$Id: cust_pkg.pm,v 1.13 2001-11-03 17:49:52 ivan Exp $
 
 =head1 BUGS
 
index daec79f..c398e5e 100644 (file)
@@ -14,6 +14,17 @@ use FS::svc_forward;
 
 @ISA = qw( FS::Record );
 
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( $hashref->{'username'} ) {
+    $self->{'_svc_acct'} = FS::svc_acct->new($hashref, '');
+  }
+  if ( $hashref->{'svc'} ) {
+    $self->{'_svcpart'} = FS::part_svc->new($hashref);
+  }
+}
+
 =head1 NAME
 
 FS::cust_svc - Object method for cust_svc objects
@@ -109,6 +120,20 @@ sub check {
   ''; #no error
 }
 
+=item part_svc
+
+Returns the definition for this service, as a FS::part_svc object (see
+L<FS::part_svc>).
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+  $self->{'_svcpart'}
+    ? $self->{'_svcpart'}
+    : qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
+}
+
 =item label
 
 Returns a list consisting of:
@@ -120,11 +145,14 @@ Returns a list consisting of:
 
 sub label {
   my $self = shift;
-  my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
-  my $svcdb = $part_svc->svcdb;
-  my $svc_x = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
-    or die "can't find $svcdb.svcnum ". $self->svcnum;
-  my $svc = $part_svc->svc;
+  my $svcdb = $self->part_svc->svcdb;
+  my $svc_x;
+  if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
+    $svc_x = $self->{'_svc_acct'};
+  } else {
+    $svc_x = qsearchs( $svcdb, { 'svcnum' => $self->svcnum } )
+      or die "can't find $svcdb.svcnum ". $self->svcnum;
+  }
   my $tag;
   if ( $svcdb eq 'svc_acct' ) {
     $tag = $svc_x->email;
@@ -148,14 +176,14 @@ sub label {
     cluck "warning: asked for label of unsupported svcdb; using svcnum";
     $tag = $svc_x->getfield('svcnum');
   }
-  $svc, $tag, $svcdb;
+  $self->part_svc->svc, $tag, $svcdb;
 }
 
 =back
 
 =head1 VERSION
 
-$Id: cust_svc.pm,v 1.5 2001-09-03 22:07:38 ivan Exp $
+$Id: cust_svc.pm,v 1.6 2001-11-03 17:49:52 ivan Exp $
 
 =head1 BUGS
 
index 3e7230f..219d8d4 100644 (file)
@@ -85,6 +85,18 @@ $FS::UID::callback{'FS::svc_acct'} = sub {
 
 #not needed in 5.004 #srand($$|time);
 
+sub _cache {
+  my $self = shift;
+  my ( $hashref, $cache ) = @_;
+  if ( $hashref->{'svc_acct_svcnum'} ) {
+    $self->{'_domsvc'} = FS::svc_domain->new( {
+      'svcnum'   => $hashref->{'domsvc'},
+      'domain'   => $hashref->{'svc_acct_domain'},
+      'catchall' => $hashref->{'svc_acct_catchall'},
+    } );
+  }
+}
+
 =head1 NAME
 
 FS::svc_acct - Object methods for svc_acct records
@@ -880,7 +892,8 @@ Returns the domain associated with this account.
 sub domain {
   my $self = shift;
   if ( $self->domsvc ) {
-    my $svc_domain = qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } )
+    #$self->svc_domain->domain;
+    my $svc_domain = $self->svc_domain
       or die "no svc_domain.svcnum for svc_acct.domsvc ". $self->domsvc;
     $svc_domain->domain;
   } else {
@@ -888,6 +901,20 @@ sub domain {
   }
 }
 
+=item svc_domain
+
+Returns the FS::svc_domain record for this account's domain (see
+L<FS::svc_domain).
+
+=cut
+
+sub svc_domain {
+  my $self = shift;
+  $self->{'_domsvc'}
+    ? $self->{'_domsvc'}
+    : qsearchs( 'svc_domain', { 'svcnum' => $self->domsvc } );
+}
+
 =item email
 
 Returns an email address associated with the account.
@@ -931,7 +958,7 @@ sub ssh {
 
 =head1 VERSION
 
-$Id: svc_acct.pm,v 1.52 2001-10-24 15:29:30 ivan Exp $
+$Id: svc_acct.pm,v 1.53 2001-11-03 17:49:52 ivan Exp $
 
 =head1 BUGS
 
index 4254514..c83fad5 100644 (file)
@@ -14,6 +14,7 @@ FS/CGI.pm
 FS/Conf.pm
 FS/ConfItem.pm
 FS/Record.pm
+FS/SearchCache.pm
 FS/UI/Base.pm
 FS/UI/CGI.pm
 FS/UI/Gtk.pm
diff --git a/FS/t/SearchCache.t b/FS/t/SearchCache.t
new file mode 100644 (file)
index 0000000..3c26f35
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::SearchCache;
+$loaded=1;
+print "ok 1\n";
index a52321d..db33df7 100755 (executable)
@@ -1,5 +1,5 @@
 <%
-#<!-- $Id: cust_main.cgi,v 1.13 2001-11-01 00:16:24 ivan Exp $ -->
+#<!-- $Id: cust_main.cgi,v 1.14 2001-11-03 17:49:52 ivan Exp $ -->
 
 use strict;
 #use vars qw( $conf %ncancelled_pkgs %all_pkgs $cgi @cust_main $sortby );
@@ -9,7 +9,7 @@ use CGI::Carp qw(fatalsToBrowser);
 use IO::Handle;
 use String::Approx qw(amatch);
 use FS::UID qw(cgisuidsetup);
-use FS::Record qw(qsearch qsearchs dbdef);
+use FS::Record qw(qsearch qsearchs dbdef jsearch);
 use FS::CGI qw(header menubar eidiot popurl table);
 use FS::cust_main;
 use FS::cust_svc;
@@ -19,17 +19,59 @@ cgisuidsetup($cgi);
 
 $conf = new FS::Conf;
 
+my $cache;
+
+#my $monsterjoin = <<END;
+#cust_main left outer join (
+#  ( cust_pkg left outer join part_pkg using(pkgpart)
+#  ) left outer join (
+#    (
+#      (
+#        ( cust_svc left outer join part_svc using (svcpart)
+#        ) left outer join svc_acct using (svcnum)
+#      ) left outer join svc_domain using(svcnum)
+#    ) left outer join svc_forward using(svcnum)
+#  ) using (pkgnum)
+#) using (custnum)
+#END
+
+my $monsterjoin = <<END;
+cust_main left outer join (
+  ( cust_pkg left outer join part_pkg using(pkgpart)
+  ) left outer join (
+    (
+      (
+        ( cust_svc left outer join part_svc using (svcpart)
+        ) left outer join (
+          svc_acct left outer join (
+            select svcnum, domain, catchall from svc_domain
+            ) as svc_acct_domsvc (
+              svc_acct_svcnum, svc_acct_domain, svc_acct_catchall
+          ) on svc_acct.domsvc = svc_acct_domsvc.svc_acct_svcnum
+        ) using (svcnum)
+      ) left outer join svc_domain using(svcnum)
+    ) left outer join svc_forward using(svcnum)
+  ) using (pkgnum)
+) using (custnum)
+END
+
 if ( $cgi->param('browse') ) {
   my $query = $cgi->param('browse');
   if ( $query eq 'custnum' ) {
     $sortby=\*custnum_sort;
-    @cust_main=qsearch('cust_main',{});  
+#    @cust_main=qsearch('cust_main',{});  
+    ( $cache, @cust_main ) =
+      jsearch($monsterjoin, {}, '', '', 'cust_main', 'custnum' );  
   } elsif ( $query eq 'last' ) {
     $sortby=\*last_sort;
-    @cust_main=qsearch('cust_main',{});  
+#    @cust_main=qsearch('cust_main',{});  
+    ( $cache, @cust_main ) =
+      jsearch($monsterjoin, {}, '', '', 'cust_main', 'custnum' );  
   } elsif ( $query eq 'company' ) {
     $sortby=\*company_sort;
-    @cust_main=qsearch('cust_main',{});
+#    @cust_main=qsearch('cust_main',{});
+    ( $cache, @cust_main ) =
+      jsearch($monsterjoin, {}, '', '', 'cust_main', 'custnum' );  
   } else {
     die "unknown browse field $query";
   }
@@ -50,6 +92,7 @@ if ( $conf->exists('hidecancelledpackages' ) ) {
 } else {
   %all_pkgs = map { $_->custnum => [ $_->all_pkgs ] } @cust_main;
 }
+#%all_pkgs = ();
 
 if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) {
   print $cgi->redirect(popurl(2). "view/cust_main.cgi?". $cust_main[0]->custnum);
@@ -58,7 +101,7 @@ if ( scalar(@cust_main) == 1 && ! $cgi->param('referral_custnum') ) {
   eidiot "No matching customers found!\n";
 } else { 
 
-  my($total)=scalar(@cust_main);
+  my $total = scalar(@cust_main);
   print header("Customer Search Results",menubar(
     'Main Menu', popurl(2)
   )), "$total matching customers found ";
@@ -128,6 +171,7 @@ print <<END;
 END
 
   my(%saw,$cust_main);
+  my $p = popurl(2);
   foreach $cust_main (
     sort $sortby grep(!$saw{$_->custnum}++, @cust_main)
   ) {
@@ -141,13 +185,14 @@ END
     my(@lol_cust_svc);
     my($rowspan)=0;#scalar( @{$all_pkgs{$custnum}} );
     foreach ( @{$all_pkgs{$custnum}} ) {
-      my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+      #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
+      my @cust_svc = $_->cust_svc;
       push @lol_cust_svc, \@cust_svc;
       $rowspan += scalar(@cust_svc) || 1;
     }
 
     #my($rowspan) = scalar(@{$all_pkgs{$custnum}});
-    my($view) = popurl(2). "view/cust_main.cgi?$custnum";
+    my $view = $p. 'view/cust_main.cgi?'. $custnum;
     print <<END;
     <TR>
       <TD ROWSPAN=$rowspan><A HREF="$view"><FONT SIZE=-1>$custnum</FONT></A></TD>
@@ -168,10 +213,13 @@ END
 
     my($n1)='';
     foreach ( @{$all_pkgs{$custnum}} ) {
-      my($pkgnum) = ($_->pkgnum);
-      my($pkg) = $_->part_pkg->pkg;
-      my $comment = $_->part_pkg->comment;
-      my($pkgview) = popurl(2). "/view/cust_pkg.cgi?$pkgnum";
+      my $pkgnum = $_->pkgnum;
+#      my $part_pkg = qsearchs( 'part_pkg', { pkgpart => $_->pkgpart } );
+      my $part_pkg = $_->part_pkg;
+
+      my $pkg = $part_pkg->pkg;
+      my $comment = $part_pkg->comment;
+      my $pkgview = $p. 'view/cust_pkg.cgi?'. $pkgnum;
       my @cust_svc = @{shift @lol_cust_svc};
       #my(@cust_svc) = qsearch( 'cust_svc', { 'pkgnum' => $_->pkgnum } );
       my $rowspan = scalar(@cust_svc) || 1;
@@ -181,7 +229,7 @@ END
       foreach my $cust_svc ( @cust_svc ) {
          my($label, $value, $svcdb) = $cust_svc->label;
          my($svcnum) = $cust_svc->svcnum;
-         my($sview) = popurl(2). "/view";
+         my($sview) = $p.'view';
          print $n2,qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
                qq!<TD><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
          $n2="</TR><TR>";