service refactor!
authorivan <ivan>
Fri, 29 Dec 2006 08:51:34 +0000 (08:51 +0000)
committerivan <ivan>
Fri, 29 Dec 2006 08:51:34 +0000 (08:51 +0000)
53 files changed:
FS/FS/Record.pm
FS/FS/Schema.pm
FS/FS/UI/Web.pm
FS/FS/cust_main.pm
FS/FS/cust_pkg.pm
FS/FS/cust_svc.pm
FS/FS/part_svc.pm
FS/FS/pkg_svc.pm
FS/FS/registrar.pm [new file with mode: 0644]
FS/FS/svc_Common.pm
FS/FS/svc_External_Common.pm [new file with mode: 0644]
FS/FS/svc_Parent_Mixin.pm [new file with mode: 0644]
FS/FS/svc_acct.pm
FS/FS/svc_broadband.pm
FS/FS/svc_domain.pm
FS/FS/svc_external.pm
FS/FS/svc_forward.pm
FS/FS/svc_phone.pm
FS/FS/svc_www.pm
FS/MANIFEST
FS/t/registrar.t [new file with mode: 0644]
FS/t/svc_External_Common.t [new file with mode: 0644]
FS/t/svc_Parent_Mixin.t [new file with mode: 0644]
eg/table_template-svc.pm
httemplate/browse/part_svc.cgi
httemplate/edit/elements/edit.html
httemplate/edit/elements/svc_Common.html
httemplate/edit/part_svc.cgi
httemplate/edit/process/elements/process.html
httemplate/edit/process/svc_Common.html [new file with mode: 0644]
httemplate/edit/svc_Common.html [new file with mode: 0644]
httemplate/edit/svc_acct.cgi
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_domain.cgi
httemplate/edit/svc_external.cgi
httemplate/edit/svc_forward.cgi
httemplate/edit/svc_www.cgi
httemplate/elements/header.html
httemplate/elements/menu.html
httemplate/misc/link.cgi
httemplate/search/cust_main.cgi
httemplate/search/cust_svc.html [new file with mode: 0644]
httemplate/search/svc_acct.cgi
httemplate/search/svc_broadband.cgi
httemplate/search/svc_domain.cgi
httemplate/search/svc_external.cgi
httemplate/search/svc_forward.cgi
httemplate/search/svc_phone.cgi
httemplate/search/svc_www.cgi
httemplate/view/cust_main/packages.html
httemplate/view/elements/svc_Common.html
rt/html/Elements/FreesideSearch
rt/html/Elements/FreesideSvcSearch

index c25b9be..ba03091 100644 (file)
@@ -974,21 +974,9 @@ returns the error, otherwise returns false.
 =cut
 
 sub replace {
 =cut
 
 sub replace {
-  my $new = shift;
-  my $old = shift;  
-
-  if (!defined($old)) { 
-    warn "[debug]$me replace called with no arguments; autoloading old record\n"
-     if $DEBUG;
-    my $primary_key = $new->dbdef_table->primary_key;
-    if ( $primary_key ) {
-      $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } )
-        or croak "can't find ". $new->table. ".$primary_key ".
-                 $new->$primary_key();
-    } else {
-      croak $new->table. " has no primary key; pass old record as argument";
-    }
-  }
+  my ($new, $old) = (shift, shift);
+
+  $old = $new->replace_old unless defined($old);
 
   warn "[debug]$me $new ->replace $old\n" if $DEBUG;
 
 
   warn "[debug]$me $new ->replace $old\n" if $DEBUG;
 
@@ -1158,6 +1146,22 @@ sub replace {
 
 }
 
 
 }
 
+sub replace_old {
+  my( $self ) = shift;
+  warn "[$me] replace called with no arguments; autoloading old record\n"
+    if $DEBUG;
+
+  my $primary_key = $self->dbdef_table->primary_key;
+  if ( $primary_key ) {
+    $self->by_key( $self->$primary_key() ) #this is what's returned
+      or croak "can't find ". $self->table. ".$primary_key ".
+        $self->$primary_key();
+  } else {
+    croak $self->table. " has no primary key; pass old record as argument";
+  }
+
+}
+
 =item rep
 
 Depriciated (use replace instead).
 =item rep
 
 Depriciated (use replace instead).
index 255b344..33baa0a 100644 (file)
@@ -867,13 +867,20 @@ sub tables_hashref {
 
     'svc_domain' => {
       'columns' => [
 
     'svc_domain' => {
       'columns' => [
-        'svcnum',    'int',    '',   '', '', '', 
-        'domain',    'varchar',    '',   $char_d, '', '', 
-        'catchall',  'int', 'NULL',    '', '', '', 
+        'svcnum',           'int',    '',        '', '', '',
+        'domain',       'varchar',    '',   $char_d, '', '',
+       'suffix',       'varchar', 'NULL',  $char_d, '', '',
+        'catchall',         'int', 'NULL',       '', '', '',
+       'parent_svcnum',    'int', 'NULL',       '', '', '',
+       'registrarnum',     'int', 'NULL',       '', '', '',
+       'registrarkey', 'varchar', 'NULL',       '', '', '',
+       'setup_date',  @date_type, '', '',
+       'renewal_interval', 'int', 'NULL',       '', '', '',
+       'expiration_date', @date_type, '', '',
       ],
       'primary_key' => 'svcnum',
       ],
       'primary_key' => 'svcnum',
-      'unique' => [ ['domain'] ],
-      'index' => [],
+      'unique' => [ ],
+      'index' => [ ['domain'] ],
     },
 
     'domain_record' => {
     },
 
     'domain_record' => {
@@ -890,6 +897,16 @@ sub tables_hashref {
       'index'       => [ ['svcnum'] ],
     },
 
       'index'       => [ ['svcnum'] ],
     },
 
+    'registrar' => {
+      'columns' => [
+        'registrarnum',   'serial', '',      '', '', '',
+       'registrarname', 'varchar', '', $char_d, '', '',
+      ],
+      'primary_key' => 'registrarnum',
+      'unique'      => [],
+      'index'       => [],
+    },
+
     'svc_forward' => {
       'columns' => [
         'svcnum',   'int',            '',   '', '', '', 
     'svc_forward' => {
       'columns' => [
         'svcnum',   'int',            '',   '', '', '', 
index 0597a38..3348d67 100644 (file)
@@ -1,6 +1,7 @@
 package FS::UI::Web;
 
 package FS::UI::Web;
 
-use vars qw($DEBUG);
+use strict;
+use vars qw($DEBUG $me);
 use FS::Conf;
 use FS::Record qw(dbdef);
 
 use FS::Conf;
 use FS::Record qw(dbdef);
 
@@ -9,6 +10,11 @@ use FS::Record qw(dbdef);
 #@ISA = qw( FS::UI );
 
 $DEBUG = 0;
 #@ISA = qw( FS::UI );
 
 $DEBUG = 0;
+$me = '[FS::UID::Web]';
+
+###
+# date parsing
+###
 
 use Date::Parse;
 sub parse_beginning_ending {
 
 use Date::Parse;
 sub parse_beginning_ending {
@@ -32,6 +38,109 @@ sub parse_beginning_ending {
   ( $beginning, $ending );
 }
 
   ( $beginning, $ending );
 }
 
+=item svc_url
+
+Returns a service URL, first checking to see if there is a service-specific
+page to link to, otherwise to a generic service handling page.  Options are
+passed as a list of name-value pairs, and include:
+
+=over 4
+
+=item * m - Mason request object ($m)
+
+=item * action - The action for which to construct "edit", "view", or "search"
+
+=item ** part_svc - Service definition (see L<FS::part_svc>)
+
+=item ** svcdb - Service table
+
+=item *** query - Query string
+
+=item *** svc   - FS::cust_svc or FS::svc_* object
+
+=item ahref - Optional flag, if set true returns <A HREF="$url"> instead of just the URL.
+
+=back 
+
+* Required fields
+
+** part_svc OR svcdb is required
+
+*** query OR svc is required
+
+=cut
+
+  # ##
+  # #required
+  # ##
+  #  'm'        => $m, #mason request object
+  #  'action'   => 'edit', #or 'view'
+  #
+  #  'part_svc' => $part_svc, #usual
+  #   #OR
+  #  'svcdb'    => 'svc_table',
+  #
+  #  'query'    => #optional query string
+  #   #OR
+  #  'svc'      => $svc_x, #or $cust_svc, it just needs a svcnum
+  #
+  # ##
+  # #optional
+  # ##
+  #  'ahref'    => 1, # if set true, returns <A HREF="$url">
+
+use FS::CGI qw(popurl);
+sub svc_url {
+  my %opt = @_;
+
+  #? return '' unless ref($opt{part_svc});
+
+  my $svcdb = $opt{svcdb} || $opt{part_svc}->svcdb;
+  my $query = exists($opt{query}) ? $opt{query} : $opt{svc}->svcnum;
+  my $url;
+  warn "$me [svc_url] checking for /$opt{action}/$svcdb.cgi component"
+    if $DEBUG;
+  if ( $opt{m}->interp->comp_exists("/$opt{action}/$svcdb.cgi") ) {
+    $url = "$svcdb.cgi?";
+  } else {
+
+    my $generic = $opt{action} eq 'search' ? 'cust_svc' : 'svc_Common';
+
+    $url = "$generic.html?svcdb=$svcdb;";
+    $url .= 'svcnum=' if $query =~ /^\d+(;|$)/;
+  }
+
+  my $p = popurl(2); #?
+  my $return = "$p$opt{action}/$url$query";
+
+  $return = qq!<A HREF="$return">! if $opt{ahref};
+
+  $return;
+}
+
+sub svc_link {
+  my($m, $part_svc, $cust_svc) = @_ or return '';
+  svc_X_link( $part_svc->svc, @_ );
+}
+
+sub svc_label_link {
+  my($m, $part_svc, $cust_svc) = @_ or return '';
+  svc_X_link( ($cust_svc->label)[1], @_ );
+}
+
+sub svc_X_link {
+  my ($x, $m, $part_svc, $cust_svc) = @_ or return '';
+  my $ahref = svc_url(
+    'ahref'    => 1,
+    'm'        => $m,
+    'action'   => 'view',
+    'part_svc' => $part_svc,
+    'svc'      => $cust_svc,
+  );
+
+  "$ahref$x</A>";
+}
+
 sub parse_lt_gt {
   my($cgi, $field) = @_;
 
 sub parse_lt_gt {
   my($cgi, $field) = @_;
 
index ebe4c24..32da745 100644 (file)
@@ -1526,11 +1526,17 @@ Returns all packages (see L<FS::cust_pkg>) for this customer.
 
 sub all_pkgs {
   my $self = shift;
 
 sub all_pkgs {
   my $self = shift;
+
+  return $self->num_pkgs unless wantarray;
+
+  my @cust_pkg = ();
   if ( $self->{'_pkgnum'} ) {
   if ( $self->{'_pkgnum'} ) {
-    values %{ $self->{'_pkgnum'}->cache };
+    @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
   } else {
   } else {
-    qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
+    @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
   }
   }
+
+  sort sort_packages @cust_pkg;
 }
 
 =item ncancelled_pkgs
 }
 
 =item ncancelled_pkgs
@@ -1541,19 +1547,43 @@ Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
 
 sub ncancelled_pkgs {
   my $self = shift;
 
 sub ncancelled_pkgs {
   my $self = shift;
+
+  return $self->num_ncancelled_pkgs unless wantarray;
+
+  my @cust_pkg = ();
   if ( $self->{'_pkgnum'} ) {
   if ( $self->{'_pkgnum'} ) {
-    grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
+
+    @cust_pkg = grep { ! $_->getfield('cancel') }
+                values %{ $self->{'_pkgnum'}->cache };
+
   } else {
   } else {
-    @{ [ # force list context
+
+    @cust_pkg =
       qsearch( 'cust_pkg', {
       qsearch( 'cust_pkg', {
-        'custnum' => $self->custnum,
-        'cancel'  => '',
-      }),
+                             'custnum' => $self->custnum,
+                             'cancel'  => '',
+                           });
+    push @cust_pkg,
       qsearch( 'cust_pkg', {
       qsearch( 'cust_pkg', {
-        'custnum' => $self->custnum,
-        'cancel'  => 0,
-      }),
-    ] };
+                             'custnum' => $self->custnum,
+                             'cancel'  => 0,
+                           });
+  }
+
+  sort sort_packages @cust_pkg;
+
+}
+
+# This should be generalized to use config options to determine order.
+sub sort_packages {
+  if ( $a->get('cancel') and $b->get('cancel') ) {
+    $a->pkgnum <=> $b->pkgnum;
+  } elsif ( $a->get('cancel') or $b->get('cancel') ) {
+    return -1 if $b->get('cancel');
+    return  1 if $a->get('cancel');
+    return 0;
+  } else {
+    $a->pkgnum <=> $b->pkgnum;
   }
 }
 
   }
 }
 
@@ -1602,8 +1632,11 @@ customer.
 =cut
 
 sub num_cancelled_pkgs {
 =cut
 
 sub num_cancelled_pkgs {
-  my $self = shift;
-  $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
+  shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
+}
+
+sub num_ncancelled_pkgs {
+  shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
 }
 
 sub num_pkgs {
 }
 
 sub num_pkgs {
@@ -1737,7 +1770,7 @@ sub _banned_pay_hashref {
   {
     'payby'   => $payby2ban{$self->payby},
     'payinfo' => md5_base64($self->payinfo),
   {
     'payby'   => $payby2ban{$self->payby},
     'payinfo' => md5_base64($self->payinfo),
-    #'reason'  =>
+    #don't ever *search* on reason! #'reason'  =>
   };
 }
 
   };
 }
 
index 6807baf..689ffed 100644 (file)
@@ -1,7 +1,7 @@
 package FS::cust_pkg;
 
 use strict;
 package FS::cust_pkg;
 
 use strict;
-use vars qw(@ISA $disable_agentcheck @SVCDB_CANCEL_SEQ $DEBUG);
+use vars qw(@ISA $disable_agentcheck $DEBUG);
 use Tie::IxHash;
 use FS::UID qw( getotaker dbh );
 use FS::Misc qw( send_email );
 use Tie::IxHash;
 use FS::UID qw( getotaker dbh );
 use FS::Misc qw( send_email );
@@ -15,6 +15,7 @@ use FS::pkg_svc;
 use FS::cust_bill_pkg;
 use FS::h_cust_svc;
 use FS::reg_code;
 use FS::cust_bill_pkg;
 use FS::h_cust_svc;
 use FS::reg_code;
+use FS::part_svc;
 use FS::cust_pkg_reason;
 
 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
 use FS::cust_pkg_reason;
 
 # need to 'use' these instead of 'require' in sub { cancel, suspend, unsuspend,
@@ -34,14 +35,6 @@ $DEBUG = 0;
 
 $disable_agentcheck = 0;
 
 
 $disable_agentcheck = 0;
 
-# The order in which to unprovision services.
-@SVCDB_CANCEL_SEQ = qw( svc_external
-                       svc_www
-                       svc_forward 
-                       svc_acct 
-                       svc_domain 
-                       svc_broadband );
-
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
 sub _cache {
   my $self = shift;
   my ( $hashref, $cache ) = @_;
@@ -273,6 +266,10 @@ Calls
 sub replace {
   my( $new, $old, %options ) = @_;
 
 sub replace {
   my( $new, $old, %options ) = @_;
 
+  # We absolutely have to have an old vs. new record to make this work.
+  if (!defined($old)) {
+    $old = qsearchs( 'cust_pkg', { 'pkgnum' => $new->pkgnum } );
+  }
   #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
   return "Can't change otaker!" if $old->otaker ne $new->otaker;
 
   #return "Can't (yet?) change pkgpart!" if $old->pkgpart != $new->pkgpart;
   return "Can't change otaker!" if $old->otaker ne $new->otaker;
 
@@ -452,19 +449,18 @@ sub cancel {
 
   my %svc;
   foreach my $cust_svc (
 
   my %svc;
   foreach my $cust_svc (
-      qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
+    #schwartz
+    map  { $_->[0] }
+    sort { $a->[1] <=> $b->[1] }
+    map  { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; }
+    qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } )
   ) {
   ) {
-    push @{ $svc{$cust_svc->part_svc->svcdb} }, $cust_svc;
-  }
 
 
-  foreach my $svcdb (@SVCDB_CANCEL_SEQ) {
-    foreach my $cust_svc (@{ $svc{$svcdb} }) {
-      my $error = $cust_svc->cancel;
+    my $error = $cust_svc->cancel;
 
 
-      if ( $error ) {
-       $dbh->rollback if $oldAutoCommit;
-       return "Error cancelling cust_svc: $error";
-      }
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "Error cancelling cust_svc: $error";
     }
   }
 
     }
   }
 
@@ -762,6 +758,17 @@ sub calc_cancel {
   $self->part_pkg->calc_cancel($self, @_);
 }
 
   $self->part_pkg->calc_cancel($self, @_);
 }
 
+=item cust_bill_pkg
+
+Returns any invoice line items for this package (see L<FS::cust_bill_pkg>).
+
+=cut
+
+sub cust_bill_pkg {
+  my $self = shift;
+  qsearch( 'cust_bill_pkg', { 'pkgnum' => $self->pkgnum } );
+}
+
 =item cust_svc [ SVCPART ]
 
 Returns the services for this package, as FS::cust_svc objects (see
 =item cust_svc [ SVCPART ]
 
 Returns the services for this package, as FS::cust_svc objects (see
@@ -843,7 +850,7 @@ sub num_cust_svc {
 
 =item available_part_svc 
 
 
 =item available_part_svc 
 
-Returns a list FS::part_svc objects representing services included in this
+Returns a list of FS::part_svc objects representing services included in this
 package but not yet provisioned.  Each FS::part_svc object also has an extra
 field, I<num_avail>, which specifies the number of available services.
 
 package but not yet provisioned.  Each FS::part_svc object also has an extra
 field, I<num_avail>, which specifies the number of available services.
 
@@ -861,6 +868,86 @@ sub available_part_svc {
       $self->part_pkg->pkg_svc;
 }
 
       $self->part_pkg->pkg_svc;
 }
 
+=item 
+
+Returns a list of FS::part_svc objects representing provisioned and available
+services included in this package.  Each FS::part_svc object also has the
+following extra fields:
+
+=over 4
+
+=item num_cust_svc  (count)
+
+=item num_avail     (quantity - count)
+
+=item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects
+
+svcnum
+label -> ($cust_svc->label)[1]
+
+=back
+
+=cut
+
+sub part_svc {
+  my $self = shift;
+
+  #XXX some sort of sort order besides numeric by svcpart...
+  my @part_svc = sort { $a->svcpart <=> $b->svcpart } map {
+    my $pkg_svc = $_;
+    my $part_svc = $pkg_svc->part_svc;
+    my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
+    $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #more evil
+    $part_svc->{'Hash'}{'num_avail'}    = $pkg_svc->quantity - $num_cust_svc;
+    $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+    $part_svc;
+  } $self->part_pkg->pkg_svc;
+
+  #extras
+  push @part_svc, map {
+    my $part_svc = $_;
+    my $num_cust_svc = $self->num_cust_svc($part_svc->svcpart);
+    $part_svc->{'Hash'}{'num_cust_svc'} = $num_cust_svc; #speak no evail
+    $part_svc->{'Hash'}{'num_avail'}    = 0; #0-$num_cust_svc ?
+    $part_svc->{'Hash'}{'cust_pkg_svc'} = [ $self->cust_svc($part_svc->svcpart) ];
+    $part_svc;
+  } $self->extra_part_svc;
+
+  @part_svc;
+
+}
+
+=item extra_part_svc
+
+Returns a list of FS::part_svc objects corresponding to services in this
+package which are still provisioned but not (any longer) available in the
+package definition.
+
+=cut
+
+sub extra_part_svc {
+  my $self = shift;
+
+  my $pkgnum  = $self->pkgnum;
+  my $pkgpart = $self->pkgpart;
+
+  qsearch( {
+    'table'     => 'part_svc',
+    'hashref'   => {},
+    'extra_sql' => "WHERE 0 = ( SELECT COUNT(*) FROM pkg_svc 
+                                  WHERE pkg_svc.svcpart = part_svc.svcpart 
+                                   AND pkg_svc.pkgpart = $pkgpart
+                                   AND quantity > 0 
+                             )
+                     AND 0 < ( SELECT count(*)
+                                 FROM cust_svc
+                                   LEFT JOIN cust_pkg using ( pkgnum )
+                                 WHERE cust_svc.svcpart = part_svc.svcpart
+                                   AND pkgnum = $pkgnum
+                             )",
+  } );
+}
+
 =item status
 
 Returns a short status string for this package, currently:
 =item status
 
 Returns a short status string for this package, currently:
index a760761..cdb34cd 100644 (file)
@@ -2,24 +2,21 @@ package FS::cust_svc;
 
 use strict;
 use vars qw( @ISA $DEBUG $me $ignore_quantity );
 
 use strict;
 use vars qw( @ISA $DEBUG $me $ignore_quantity );
-use Carp qw( carp cluck );
+use Carp;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
 use FS::Conf;
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_pkg;
 use FS::part_pkg;
 use FS::part_svc;
 use FS::pkg_svc;
-use FS::svc_acct;
-use FS::svc_domain;
-use FS::svc_forward;
-use FS::svc_broadband;
-use FS::svc_phone;
-use FS::svc_external;
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
 
 use FS::domain_record;
 use FS::part_export;
 use FS::cdr;
 
-@ISA = qw( FS::Record );
+#most FS::svc_ classes are autoloaded in svc_x emthod
+use FS::svc_acct;  #this one is used in the cache stuff
+
+@ISA = qw( FS::cust_main_Mixin FS::Record );
 
 $DEBUG = 0;
 $me = '[cust_svc]';
 
 $DEBUG = 0;
 $me = '[cust_svc]';
@@ -289,54 +286,20 @@ sub label {
   my $self = shift;
   carp "FS::cust_svc::label called on $self" if $DEBUG;
   my $svc_x = $self->svc_x
   my $self = shift;
   carp "FS::cust_svc::label called on $self" if $DEBUG;
   my $svc_x = $self->svc_x
-    or die "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+    or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+
   $self->_svc_label($svc_x);
 }
 
 sub _svc_label {
   my( $self, $svc_x ) = ( shift, shift );
   $self->_svc_label($svc_x);
 }
 
 sub _svc_label {
   my( $self, $svc_x ) = ( shift, shift );
-  my $svcdb = $self->part_svc->svcdb;
-
-  my $tag;
-  if ( $svcdb eq 'svc_acct' ) {
-    $tag = $svc_x->email(@_);
-  } elsif ( $svcdb eq 'svc_forward' ) {
-    if ( $svc_x->srcsvc ) {
-      my $svc_acct = $svc_x->srcsvc_acct(@_);
-      $tag = $svc_acct->email(@_);
-    } else {
-      $tag = $svc_x->src;
-    }
-    $tag .= '->';
-    if ( $svc_x->dstsvc ) {
-      my $svc_acct = $svc_x->dstsvc_acct(@_);
-      $tag .= $svc_acct->email(@_);
-    } else {
-      $tag .= $svc_x->dst;
-    }
-  } elsif ( $svcdb eq 'svc_domain' ) {
-    $tag = $svc_x->getfield('domain');
-  } elsif ( $svcdb eq 'svc_www' ) {
-    my $domain_record = $svc_x->domain_record(@_);
-    $tag = $domain_record->zone;
-  } elsif ( $svcdb eq 'svc_broadband' ) {
-    $tag = $svc_x->ip_addr;
-  } elsif ( $svcdb eq 'svc_phone' ) {
-    $tag = $svc_x->phonenum; #XXX format it better
-  } elsif ( $svcdb eq 'svc_external' ) {
-    my $conf = new FS::Conf;
-    if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
-      $tag = sprintf('%010d', $svc_x->id). '-'.
-             substr('0000000000'.uc($svc_x->title), -10);
-    } else {
-      $tag = $svc_x->id. ': '. $svc_x->title;
-    }
-  } else {
-    cluck "warning: asked for label of unsupported svcdb; using svcnum";
-    $tag = $svc_x->getfield('svcnum');
-  }
 
 
-  $self->part_svc->svc, $tag, $svcdb, $self->svcnum;
+  (
+    $self->part_svc->svc,
+    $svc_x->label(@_),
+    $self->part_svc->svcdb,
+    $self->svcnum
+  );
 
 }
 
 
 }
 
@@ -353,7 +316,7 @@ sub svc_x {
   if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
     $self->{'_svc_acct'};
   } else {
   if ( $svcdb eq 'svc_acct' && $self->{'_svc_acct'} ) {
     $self->{'_svc_acct'};
   } else {
-    #require "FS/$svcdb.pm";
+    require "FS/$svcdb.pm";
     warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
          ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
       if $DEBUG;
     warn "$me svc_x: part_svc.svcpart ". $self->part_svc->svcpart.
          ", so searching for $svcdb.svcnum ". $self->svcnum. "\n"
       if $DEBUG;
index fc5258f..5b4e54c 100644 (file)
@@ -2,6 +2,7 @@ package FS::part_svc;
 
 use strict;
 use vars qw( @ISA $DEBUG );
 
 use strict;
 use vars qw( @ISA $DEBUG );
+use Tie::IxHash;
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::Schema qw( dbdef );
 use FS::part_svc_column;
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::Schema qw( dbdef );
 use FS::part_svc_column;
@@ -11,7 +12,7 @@ use FS::cust_svc;
 
 @ISA = qw(FS::Record);
 
 
 @ISA = qw(FS::Record);
 
-$DEBUG = 1;
+$DEBUG = 0;
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -500,6 +501,161 @@ sub svc_x {
   map { $_->svc_x } $self->cust_svc;
 }
 
   map { $_->svc_x } $self->cust_svc;
 }
 
+=back
+
+=head1 CLASS METHODS
+
+=over 4
+
+=cut
+
+my $svc_defs;
+sub _svc_defs {
+
+  return $svc_defs if $svc_defs; #cache
+
+  my $conf = new FS::Conf;
+
+  #false laziness w/part_pkg.pm::plan_info
+
+  my %info;
+  foreach my $INC ( @INC ) {
+    warn "globbing $INC/FS/svc_*.pm\n" if $DEBUG;
+    foreach my $file ( glob("$INC/FS/svc_*.pm") ) {
+
+      warn "attempting to load service table info from $file\n" if $DEBUG;
+      $file =~ /\/(\w+)\.pm$/ or do {
+        warn "unrecognized file in $INC/FS/: $file\n";
+        next;
+      };
+      my $mod = $1;
+
+      if ( $mod =~ /^svc_[A-Z]/ or $mod =~ /^svc_acct_pop$/ ) {
+        warn "skipping FS::$mod" if $DEBUG;
+       next;
+      }
+
+      eval "use FS::$mod;";
+      if ( $@ ) {
+        die "error using FS::$mod (skipping): $@\n" if $@;
+        next;
+      }
+      unless ( UNIVERSAL::can("FS::$mod", 'table_info') ) {
+        warn "FS::$mod has no table_info method; skipping";
+       next;
+      }
+
+      my $info = "FS::$mod"->table_info;
+      unless ( keys %$info ) {
+        warn "FS::$mod->table_info doesn't return info, skipping\n";
+        next;
+      }
+      warn "got info from FS::$mod: $info\n" if $DEBUG;
+      if ( exists($info->{'disabled'}) && $info->{'disabled'} ) {
+        warn "skipping disabled service FS::$mod" if $DEBUG;
+        next;
+      }
+      $info{$mod} = $info;
+    }
+  }
+
+  tie my %svc_defs, 'Tie::IxHash', 
+    map  { $_ => $info{$_}->{'fields'} }
+    sort { $info{$a}->{'display_weight'} <=> $info{$b}->{'display_weight'} }
+    keys %info,
+  ;
+  
+  # yuck.  maybe this won't be so bad when virtual fields become real fields
+  my %vfields;
+  foreach my $svcdb (grep dbdef->table($_), keys %svc_defs ) {
+    eval "use FS::$svcdb;";
+    my $self = "FS::$svcdb"->new;
+    $vfields{$svcdb} = {};
+    foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
+      my $pvf = $self->pvf($field);
+      my @list = $pvf->list;
+      if (scalar @list) {
+        $svc_defs{$svcdb}->{$field} = { desc        => $pvf->label,
+                                        type        => 'select',
+                                        select_list => \@list };
+      } else {
+        $svc_defs{$svcdb}->{$field} = $pvf->label;
+      } #endif
+      $vfields{$svcdb}->{$field} = $pvf;
+      warn "\$vfields{$svcdb}->{$field} = $pvf"
+        if $DEBUG;
+    } #next $field
+  } #next $svcdb
+  
+  $svc_defs = \%svc_defs; #cache
+  
+}
+
+=item svc_tables
+
+Returns a list of all svc_ tables.
+
+=cut
+
+sub svc_tables {
+  my $class = shift;
+  my $svc_defs = $class->_svc_defs;
+  grep { defined( dbdef->table($_) ) } keys %$svc_defs;
+}
+
+=item svc_table_fields TABLE
+
+Given a table name, returns a hashref of field names.  The field names
+returned are those with additional (service-definition related) information,
+not necessarily all database fields of the table.  Pseudo-fields may also
+be returned (i.e. svc_acct.usergroup).
+
+Each value of the hashref is another hashref, which can have one or more of
+the following keys:
+
+=over 4
+
+=item label - Description of the field
+
+=item def_label - Optional description of the field in the context of service definitions
+
+=item type - Currently "text", "select", "disabled", or "radius_usergroup_selector"
+
+=item disable_default - This field should not allow a default value in service definitions
+
+=item disable_fixed - This field should not allow a fixed value in service definitions
+
+=item disable_inventory - This field should not allow inventory values in service definitions
+
+=item select_list - If type is "text", this can be a listref of possible values.
+
+=item select_table - An alternative to select_list, this defines a database table with the possible choices.
+
+=item select_key - Used with select_table, this is the field name of keys
+
+=item select_label - Used with select_table, this is the field name of labels
+
+=back
+
+=cut
+
+#maybe this should move and be a class method in svc_Common.pm
+sub svc_table_fields {
+  my($class, $table) = @_;
+  my $svc_defs = $class->_svc_defs;
+  my $def = $svc_defs->{$table};
+
+  foreach ( grep !ref($def->{$_}), keys %$def ) {
+
+    #normalize the shortcut in %info hash
+    $def->{$_} = { 'label' => $def->{$_} };
+
+    $def->{$_}{'type'} ||= 'text';
+
+  }
+
+  $def;
+}
 
 =back
 
 
 =back
 
@@ -554,10 +710,7 @@ sub process {
                   }
                   @fields;
 
                   }
                   @fields;
 
-            } grep defined( dbdef->table($_) ),
-                   qw( svc_acct svc_domain svc_forward svc_www svc_broadband
-                       svc_phone svc_external
-                     )
+            } FS::part_svc->svc_tables()
       )
   } );
   
       )
   } );
   
@@ -651,8 +804,8 @@ sub process_bulk_cust_svc {
 
 Delete is unimplemented.
 
 
 Delete is unimplemented.
 
-The list of svc_* tables is hardcoded.  When svc_acct_pop is renamed, this
-should be fixed.
+The list of svc_* tables is no longer hardcoded, but svc_acct_pop is skipped
+as a special case until it is renamed.
 
 all_part_svc_column methods should be documented
 
 
 all_part_svc_column methods should be documented
 
index 065ddbe..9f3a4a1 100644 (file)
@@ -82,7 +82,9 @@ returns the error, otherwise returns false.
 =cut
 
 sub replace {
 =cut
 
 sub replace {
-  my ( $new, $old ) = ( shift, shift );
+  my( $new, $old ) = ( shift, shift );
+
+  $old = $new->replace_old unless defined($old);
 
   return "Can't change pkgpart!" if $old->pkgpart != $new->pkgpart;
   return "Can't change svcpart!" if $old->svcpart != $new->svcpart;
 
   return "Can't change pkgpart!" if $old->pkgpart != $new->pkgpart;
   return "Can't change svcpart!" if $old->svcpart != $new->svcpart;
diff --git a/FS/FS/registrar.pm b/FS/FS/registrar.pm
new file mode 100644 (file)
index 0000000..cf5dc49
--- /dev/null
@@ -0,0 +1,119 @@
+package FS::registrar;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearch qsearchs );
+
+@ISA = qw(FS::Record);
+
+=head1 NAME
+
+FS::registrar - Object methods for registrar records
+
+=head1 SYNOPSIS
+
+  use FS::registrar;
+
+  $record = new FS::registrar \%hash;
+  $record = new FS::registrar { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::registrar object represents a registrar.  FS::registrar inherits from
+FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item registrarnum - primary key
+
+=item registrarname - 
+
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new registrar.  To add the registrar to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'registrar'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid registrar.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('registrarnum')
+    || $self->ut_text('registrarname')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
index 72e7d5c..f60a7d9 100644 (file)
@@ -2,7 +2,7 @@ package FS::svc_Common;
 
 use strict;
 use vars qw( @ISA $noexport_hack $DEBUG $me );
 
 use strict;
 use vars qw( @ISA $noexport_hack $DEBUG $me );
-use Carp;
+use Carp qw( cluck carp croak ); #specify cluck have to specify them all..
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_main_Mixin;
 use FS::cust_svc;
 use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_main_Mixin;
 use FS::cust_svc;
@@ -36,6 +36,27 @@ inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
 
 =over 4
 
 
 =over 4
 
+=item search_sql_field FIELD STRING
+
+Class method which returns an SQL fragment to search for STRING in FIELD.
+
+=cut
+
+sub search_sql_field {
+  my( $class, $field, $string ) = @_;
+  my $table = $class->table;
+  my $q_string = dbh->quote($string);
+  "$table.$field = $q_string";
+}
+
+#fallback for services that don't provide a search... 
+sub search_sql {
+  #my( $class, $string ) = @_;
+  '1 = 0'; #false
+}
+
+=item new
+
 =cut
 
 sub new {
 =cut
 
 sub new {
@@ -114,6 +135,19 @@ sub virtual_fields {
   return ();
 }
 
   return ();
 }
 
+=item label
+
+svc_Common provides a fallback label subroutine that just returns the svcnum.
+
+=cut
+
+sub label {
+  my $self = shift;
+  cluck "warning: ". ref($self). " not loaded or missing label method; ".
+        "using svcnum";
+  $self->svcnum;
+}
+
 =item check
 
 Checks the validity of fields in this record.
 =item check
 
 Checks the validity of fields in this record.
@@ -300,35 +334,15 @@ sub delete {
   local $SIG{TSTP} = 'IGNORE';
   local $SIG{PIPE} = 'IGNORE';
 
   local $SIG{TSTP} = 'IGNORE';
   local $SIG{PIPE} = 'IGNORE';
 
-  my $svcnum = $self->svcnum;
-
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   my $oldAutoCommit = $FS::UID::AutoCommit;
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
-  $error = $self->SUPER::delete;
-  return $error if $error;
-
-  #new-style exports!
-  unless ( $noexport_hack ) {
-    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-      $error = $part_export->export_delete($self);
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "exporting to ". $part_export->exporttype.
-               " (transaction rolled back): $error";
-      }
-    }
-  }
-
-  $error = $self->return_inventory;
-  if ( $error ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "error returning inventory: $error";
-  }
-
-  my $cust_svc = $self->cust_svc;
-  $error = $cust_svc->delete;
+  $error =    $self->SUPER::delete
+           || $self->export('delete')
+          || $self->return_inventory
+          || $self->cust_svc->delete
+  ;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -361,18 +375,7 @@ sub replace {
   my $dbh = dbh;
 
   # We absolutely have to have an old vs. new record to make this work.
   my $dbh = dbh;
 
   # We absolutely have to have an old vs. new record to make this work.
-  if ( !defined($old) ) { 
-    warn "[$me] replace called with no arguments; autoloading old record\n"
-      if $DEBUG;
-    my $primary_key = $new->dbdef_table->primary_key;
-    if ( $primary_key ) {
-      $old = qsearchs($new->table, { $primary_key => $new->$primary_key() } )
-        or croak "can't find ". $new->table. ".$primary_key ".
-                $new->$primary_key();
-    } else {
-      croak $new->table. " has no primary key; pass old record as argument";
-    }
-  }
+  $old = $new->replace_old unless defined($old);
 
   my $error = $new->set_auto_inventory;
   if ( $error ) {
 
   my $error = $new->set_auto_inventory;
   if ( $error ) {
@@ -678,33 +681,7 @@ Runs export_suspend callbacks.
 
 sub suspend {
   my $self = shift;
 
 sub suspend {
   my $self = shift;
-
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
-  local $SIG{QUIT} = 'IGNORE';
-  local $SIG{TERM} = 'IGNORE';
-  local $SIG{TSTP} = 'IGNORE';
-  local $SIG{PIPE} = 'IGNORE';
-
-  my $oldAutoCommit = $FS::UID::AutoCommit;
-  local $FS::UID::AutoCommit = 0;
-  my $dbh = dbh;
-
-  #new-style exports!
-  unless ( $noexport_hack ) {
-    foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-      my $error = $part_export->export_suspend($self);
-      if ( $error ) {
-        $dbh->rollback if $oldAutoCommit;
-        return "error exporting to ". $part_export->exporttype.
-               " (transaction rolled back): $error";
-      }
-    }
-  }
-
-  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
-  '';
-
+  $self->export('suspend');
 }
 
 =item unsuspend
 }
 
 =item unsuspend
@@ -715,6 +692,19 @@ Runs export_unsuspend callbacks.
 
 sub unsuspend {
   my $self = shift;
 
 sub unsuspend {
   my $self = shift;
+  $self->export('unsuspend');
+}
+
+=item export HOOK [ EXPORT_ARGS ]
+
+Runs the provided export hook (i.e. "suspend", "unsuspend") for this service.
+
+=cut
+
+sub export {
+  my( $self, $method ) = ( shift, shift );
+
+  $method = "export_$method" unless $method =~ /^export_/;
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
 
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -730,10 +720,11 @@ sub unsuspend {
   #new-style exports!
   unless ( $noexport_hack ) {
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
   #new-style exports!
   unless ( $noexport_hack ) {
     foreach my $part_export ( $self->cust_svc->part_svc->part_export ) {
-      my $error = $part_export->export_unsuspend($self);
+      next unless $part_export->can($method);
+      my $error = $part_export->$method($self, @_);
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
       if ( $error ) {
         $dbh->rollback if $oldAutoCommit;
-        return "error exporting to ". $part_export->exporttype.
+        return "error exporting $method event to ". $part_export->exporttype.
                " (transaction rolled back): $error";
       }
     }
                " (transaction rolled back): $error";
       }
     }
@@ -787,6 +778,8 @@ sub clone_kludge_unsuspend {
 
 The setfixed method return value.
 
 
 The setfixed method return value.
 
+B<export> method isn't used by insert and replace methods yet.
+
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>, L<FS::cust_pkg>, schema.html
diff --git a/FS/FS/svc_External_Common.pm b/FS/FS/svc_External_Common.pm
new file mode 100644 (file)
index 0000000..a5805aa
--- /dev/null
@@ -0,0 +1,199 @@
+package FS::svc_External_Common;
+
+use strict;
+use vars qw(@ISA);
+use FS::svc_Common;
+
+@ISA = qw( FS::svc_Common );
+
+=head1 NAME
+
+FS::svc_external - Object methods for svc_external records
+
+=head1 SYNOPSIS
+
+  use FS::svc_external;
+
+  $record = new FS::svc_external \%hash;
+  $record = new FS::svc_external { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+  $error = $record->suspend;
+
+  $error = $record->unsuspend;
+
+  $error = $record->cancel;
+
+=head1 DESCRIPTION
+
+FS::svc_External_Common is intended as a base class for table-specific classes
+to inherit from.  FS::svc_External_Common is used for services which connect
+to externally tracked services via "id" and "table" fields.
+
+FS::svc_External_Common inherits from FS::svc_Common.
+
+The following fields are currently supported:
+
+=over 4
+
+=item svcnum - primary key
+
+=item id - unique number of external record
+
+=item title - for invoice line items
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item search_sql
+
+Provides a default search_sql method which returns an SQL fragment to search
+the B<title> field.
+
+=cut
+
+sub search_sql {
+  my($class, $string) = @_;
+  $class->search_sql_field('title', $string);
+}
+
+=item new HASHREF
+
+Creates a new external service.  To add the external service to the database,
+see L<"insert">.  
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+=item label
+
+Returns a string identifying this external service in the form "id:title"
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->id. ':'. $self->title;
+}
+
+=item insert [ , OPTION => VALUE ... ]
+
+Adds this external service to the database.  If there is an error, returns the
+error, otherwise returns false.
+
+The additional fields pkgnum and svcpart (see L<FS::cust_svc>) should be 
+defined.  An FS::cust_svc record will be created and inserted.
+
+Currently available options are: I<depend_jobnum>
+
+If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
+jobnums), all provisioning jobs will have a dependancy on the supplied
+jobnum(s) (they will not run until the specific job(s) complete(s)).
+
+=cut
+
+#sub insert {
+#  my $self = shift;
+#  my $error;
+#
+#  $error = $self->SUPER::insert(@_);
+#  return $error if $error;
+#
+#  '';
+#}
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+#sub delete {
+#  my $self = shift;
+#  my $error;
+#
+#  $error = $self->SUPER::delete;
+#  return $error if $error;
+#
+#  '';
+#}
+
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+#sub replace {
+#  my ( $new, $old ) = ( shift, shift );
+#  my $error;
+#
+#  $error = $new->SUPER::replace($old);
+#  return $error if $error;
+#
+#  '';
+#}
+
+=item suspend
+
+Called by the suspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item unsuspend
+
+Called by the unsuspend method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item cancel
+
+Called by the cancel method of FS::cust_pkg (see L<FS::cust_pkg>).
+
+=item check
+
+Checks all fields to make sure this is a valid external service.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $x = $self->setfixed;
+  return $x unless ref($x);
+  my $part_svc = $x;
+
+  my $error = 
+    $self->ut_numbern('svcnum')
+    || $self->ut_numbern('id')
+    || $self->ut_textn('title')
+  ;
+
+  $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
+L<FS::cust_pkg>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/FS/FS/svc_Parent_Mixin.pm b/FS/FS/svc_Parent_Mixin.pm
new file mode 100644 (file)
index 0000000..4501baf
--- /dev/null
@@ -0,0 +1,103 @@
+package FS::svc_Parent_Mixin;
+
+use strict;
+use NEXT;
+use FS::Record qw(qsearch qsearchs);
+use FS::cust_svc;
+
+=head1 NAME
+
+FS::svc_Parent_Mixin - Mixin class for svc_ classes with a parent_svcnum field
+
+=head1 SYNOPSIS
+
+package FS::svc_table;
+use vars qw(@ISA);
+@ISA = qw( FS::svc_Parent_Mixin FS::svc_Common );
+
+=head1 DESCRIPTION
+
+This is a mixin class for svc_ classes that contain a parent_svcnum field.
+
+=cut
+
+=head1 METHODS
+
+=over 4
+
+=item parent_cust_svc
+
+Returns the parent FS::cust_svc object.
+
+=cut
+
+sub parent_cust_svc {
+  my $self = shift;
+  qsearchs('cust_svc', { 'svcnum' => $self->parent_svcnum } );
+}
+
+=item parent_svc_x
+
+Returns the corresponding parent FS::svc_ object.
+
+=cut
+
+sub parent_svc_x {
+  my $self = shift;
+  $self->parent_cust_svc->svc_x;
+}
+
+=item children_cust_svc
+
+Returns a list of any child FS::cust_svc objects.
+
+Note: This is not recursive; it only returns direct children.
+
+=cut
+
+sub children_cust_svc { 
+  my $self = shift;
+  qsearch('cust_svc', { 'parent_svcnum' => $self->svcnum } );
+}
+
+=item children_svc_x
+
+Returns the corresponding list of child FS::svc_ objects.
+
+=cut
+
+sub children_svc_x {
+  my $self = shift;
+  map { $_->svc_x } $self->children_cust_svc;
+}
+
+=item check
+
+This class provides a check subroutine which takes care of checking the
+parent_svcnum field.  The svc_ class which uses it will call SUPER::check at
+the end of its own checks, and this class will call NEXT::check to pass 
+the check "up the chain" (see L<NEXT>).
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  $self->ut_foreign_keyn('parent_svcnum', 'cust_svc', 'svcnum')
+    || $self->NEXT::check;
+
+}
+
+=back
+
+=head1 BUGS
+
+Do we need a recursive child finder for multi-layered children?
+
+=head1 SEE ALSO
+
+L<FS::svc_Common>, L<FS::Record>
+
+=cut
+
+1;
index 8c1c350..7cbe63f 100644 (file)
@@ -210,6 +210,77 @@ Creates a new account.  To add the account to the database, see L<"insert">.
 
 =cut
 
 
 =cut
 
+sub table_info {
+  {
+    'name'   => 'Account',
+    'longname_plural' => 'Access accounts and mailboxes',
+    'sorts' => [ 'username', 'uid', ],
+    'display_weight' => 10,
+    'cancel_weight'  => 50, 
+    'fields' => {
+        'dir'       => 'Home directory',
+        'uid'       => {
+                         label     => 'UID',
+                        def_label => 'UID (set to fixed and blank for no UIDs)',
+                        type      => 'text',
+                      },
+        'slipip'    => 'IP address',
+    #    'popnum'    => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
+        'popnum'    => {
+                         label => 'Access number',
+                         type => 'select',
+                         select_table => 'svc_acct_pop',
+                         select_key   => 'popnum',
+                         select_label => 'city',
+                       },
+        'username'  => {
+                         label => 'Username',
+                         type => 'text',
+                         disable_default => 1,
+                         disable_fixed => 1,
+                       },
+        'quota'     => { 
+                         label => 'Quota',
+                         type => 'text',
+                         disable_inventory => 1,
+                       },
+        '_password' => 'Password',
+        'gid'       => {
+                         label     => 'GID',
+                        def_label => 'GID (when blank, defaults to UID)',
+                        type      => 'text',
+                      },
+        'shell'     => {
+                         #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file, set to blank for no shell tracking)',
+                        label    => 'Shell',
+                         def_label=> 'Shell (set to blank for no shell tracking)',
+                         type     =>'select',
+                         select_list => [ $conf->config('shells') ],
+                         disable_inventory => 1,
+                       },
+        'finger'    => 'Real name (GECOS)',
+        'domsvc'    => {
+                         label     => 'Domain',
+                         def_label => 'svcnum from svc_domain',
+                         type      => 'select',
+                         select_table => 'svc_domain',
+                         select_key   => 'svcnum',
+                         select_label => 'domain',
+                         disable_inventory => 1,
+                       },
+        'usergroup' => {
+                         label => 'RADIUS groups',
+                         type  => 'radius_usergroup_selector',
+                         disable_inventory => 1,
+                       },
+        'seconds'   => { label => 'Seconds',
+                         type  => 'text',
+                         disable_inventory => 1,
+                       },
+    },
+  };
+}
+
 sub table { 'svc_acct'; }
 
 sub _fieldhandlers {
 sub table { 'svc_acct'; }
 
 sub _fieldhandlers {
@@ -228,6 +299,52 @@ sub _fieldhandlers {
   };
 }
 
   };
 }
 
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+  my( $class, $string ) = @_;
+  if ( $string =~ /^([^@]+)@([^@]+)$/ ) {
+    my( $username, $domain ) = ( $1, $2 );
+    my $q_username = dbh->quote($username);
+    my @svc_domain = qsearch('svc_domain', { 'domain' => $domain } );
+    if ( @svc_domain ) {
+      "svc_acct.username = $q_username AND ( ".
+        join( ' OR ', map { "svc_acct.domsvc = ". $_->svcnum; } @svc_domain ).
+      " )";
+    } else {
+      '1 = 0'; #false
+    }
+  } elsif ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
+    ' ( '.
+      $class->search_sql_field('slipip',   $string ).
+    ' OR '.
+      $class->search_sql_field('username', $string ).
+    ' ) ';
+  } else {
+    $class->search_sql_field('username', $string);
+  }
+}
+
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns the "username@domain" string for this account.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->email(@_);
+}
+
+=cut
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this account to the database.  If there is an error, returns the error,
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this account to the database.  If there is an error, returns the error,
@@ -1180,10 +1297,13 @@ sub forget_snapshot {
 
 }
 
 
 }
 
-=item domain
+=item domain [ END_TIMESTAMP [ START_TIMESTAMP ] ]
 
 Returns the domain associated with this account.
 
 
 Returns the domain associated with this account.
 
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
 =cut
 
 sub domain {
 =cut
 
 sub domain {
@@ -1201,6 +1321,8 @@ L<FS::svc_domain>).
 
 =cut
 
 
 =cut
 
+# FS::h_svc_acct has a history-aware svc_domain override
+
 sub svc_domain {
   my $self = shift;
   $self->{'_domsvc'}
 sub svc_domain {
   my $self = shift;
   $self->{'_domsvc'}
@@ -1216,10 +1338,13 @@ Returns the FS::cust_svc record for this account (see L<FS::cust_svc>).
 
 #inherited from svc_Common
 
 
 #inherited from svc_Common
 
-=item email
+=item email [ END_TIMESTAMP [ START_TIMESTAMP ] ]
 
 Returns an email address associated with the account.
 
 
 Returns an email address associated with the account.
 
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
 =cut
 
 sub email {
 =cut
 
 sub email {
index 5c9fe5e..b047c9a 100755 (executable)
@@ -85,8 +85,50 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
 
 =cut
 
+sub table_info {
+  {
+    'name' => 'Broadband',
+    'name_plural' => 'Broadband services',
+    'longname_plural' => 'Fixed (username-less) broadband services',
+    'display_weight' => 50,
+    'cancel_weight'  => 70,
+    'fields' => {
+      'speed_down' => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
+      'speed_up'   => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
+      'ip_addr'    => 'IP address.  Leave blank for automatic assignment.',
+      'blocknum'   => 'Address block.',
+    },
+  };
+}
+
 sub table { 'svc_broadband'; }
 
 sub table { 'svc_broadband'; }
 
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+  my( $class, $string ) = @_;
+  if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
+    $class->search_sql_field('ip_addr', $string );
+  } else {
+    '1 = 0'; #false
+  }
+}
+
+=item label
+
+Returns the IP address.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->ip_addr;
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this record to the database.  If there is an error, returns the error,
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this record to the database.  If there is an error, returns the error,
index bdaf79b..157f9e0 100644 (file)
@@ -11,6 +11,7 @@ use Date::Format;
 use FS::Record qw(fields qsearch qsearchs dbh);
 use FS::Conf;
 use FS::svc_Common;
 use FS::Record qw(fields qsearch qsearchs dbh);
 use FS::Conf;
 use FS::svc_Common;
+use FS::svc_Parent_Mixin;
 use FS::cust_svc;
 use FS::svc_acct;
 use FS::cust_pkg;
 use FS::cust_svc;
 use FS::svc_acct;
 use FS::cust_pkg;
@@ -18,7 +19,7 @@ use FS::cust_main;
 use FS::domain_record;
 use FS::queue;
 
 use FS::domain_record;
 use FS::queue;
 
-@ISA = qw( FS::svc_Common );
+@ISA = qw( FS::svc_Parent_Mixin FS::svc_Common );
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::domain'} = sub { 
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::domain'} = sub { 
@@ -72,6 +73,20 @@ FS::svc_Common.  The following fields are currently supported:
 
 =item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
 
 
 =item catchall - optional svcnum of an svc_acct record, designating an email catchall account.
 
+=item suffix - 
+
+=item parent_svcnum -
+
+=item registrarnum - Registrar (see L<FS::registrar>)
+
+=item registrarkey - Registrar key or password for this domain
+
+=item setup_date - UNIX timestamp
+
+=item renewal_interval - Number of days before expiration date to start renewal
+
+=item expiration_date - UNIX timestamp
+
 =back
 
 =head1 METHODS
 =back
 
 =head1 METHODS
@@ -84,8 +99,37 @@ Creates a new domain.  To add the domain to the database, see L<"insert">.
 
 =cut
 
 
 =cut
 
+sub table_info {
+  {
+    'name' => 'Domain',
+    'sorts' => 'domain',
+    'display_weight' => 20,
+    'cancel_weight'  => 60,
+    'fields' => {
+      'domain' => 'Domain',
+    },
+  };
+}
+
 sub table { 'svc_domain'; }
 
 sub table { 'svc_domain'; }
 
+sub search_sql {
+  my($class, $string) = @_;
+  $class->search_sql_field('domain', $string);
+}
+
+
+=item label
+
+Returns the domain.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->domain;
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this domain to the database.  If there is an error, returns the error,
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this domain to the database.  If there is an error, returns the error,
@@ -141,15 +185,6 @@ sub insert {
   return "Domain in use (here)"
     if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
 
   return "Domain in use (here)"
     if qsearchs( 'svc_domain', { 'domain' => $self->domain } );
 
-  my $whois = $self->whois;
-  if ( $self->action eq "N" && ! $whois_hack && $whois ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Domain in use (see whois)";
-  }
-  if ( $self->action eq "M" && ! $whois ) {
-    $dbh->rollback if $oldAutoCommit;
-    return "Domain not found (see whois)";
-  }
 
   $error = $self->SUPER::insert(@_);
   if ( $error ) {
 
   $error = $self->SUPER::insert(@_);
   if ( $error ) {
@@ -157,8 +192,6 @@ sub insert {
     return $error;
   }
 
     return $error;
   }
 
-  $self->submit_internic unless $whois_hack;
-
   if ( $soamachine ) {
     my $soa = new FS::domain_record {
       'svcnum'  => $self->svcnum,
   if ( $soamachine ) {
     my $soa = new FS::domain_record {
       'svcnum'  => $self->svcnum,
@@ -257,6 +290,9 @@ returns the error, otherwise returns false.
 sub replace {
   my ( $new, $old ) = ( shift, shift );
 
 sub replace {
   my ( $new, $old ) = ( shift, shift );
 
+  # We absolutely have to have an old vs. new record to make this work.
+  $old = $new->replace_old unless defined($old);
+
   return "Can't change domain - reorder."
     if $old->getfield('domain') ne $new->getfield('domain'); 
 
   return "Can't change domain - reorder."
     if $old->getfield('domain') ne $new->getfield('domain'); 
 
@@ -317,45 +353,32 @@ sub check {
 
   my($recref) = $self->hashref;
 
 
   my($recref) = $self->hashref;
 
-  unless ( $whois_hack ) {
-    unless ( $self->email ) { #find out an email address
-      my @svc_acct;
-      foreach ( qsearch( 'cust_svc', { 'pkgnum' => $pkgnum } ) ) {
-        my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $_->svcnum } );
-        push @svc_acct, $svc_acct if $svc_acct;
-      }
-
-      if ( scalar(@svc_acct) == 0 ) {
-        return "Must order an account in package ". $pkgnum. " first";
-      } elsif ( scalar(@svc_acct) > 1 ) {
-        return "More than one account in package ". $pkgnum. ": specify admin contact email";
-      } else {
-        $self->email($svc_acct[0]->email );
-      }
-    }
-  }
-
   #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
   #if ( $recref->{domain} =~ /^([\w\-\.]{1,22})\.(com|net|org|edu)$/ ) {
-  if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu)$/ ) {
+  if ( $recref->{domain} =~ /^([\w\-]{1,63})\.(com|net|org|edu|tv|info|biz)$/ ) {
     $recref->{domain} = "$1.$2";
     $recref->{domain} = "$1.$2";
+    $recref->{suffix} ||= $2;
   # hmmmmmmmm.
   # hmmmmmmmm.
-  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)$/ ) {
-    $recref->{domain} = $1;
+  } elsif ( $whois_hack && $recref->{domain} =~ /^([\w\-\.]+)\.(\w+)$/ ) {
+    $recref->{domain} = "$1.$2";
+    # need to match a list of suffixes - no guarantee they're top-level..
   } else {
     return "Illegal domain ". $recref->{domain}.
            " (or unknown registry - try \$whois_hack)";
   }
 
   } else {
     return "Illegal domain ". $recref->{domain}.
            " (or unknown registry - try \$whois_hack)";
   }
 
-  $recref->{action} =~ /^(M|N)$/
-    or return "Illegal action: ". $recref->{action};
-  $recref->{action} = $1;
 
   if ( $recref->{catchall} ne '' ) {
     my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
     return "Unknown catchall" unless $svc_acct;
   }
 
 
   if ( $recref->{catchall} ne '' ) {
     my $svc_acct = qsearchs( 'svc_acct', { 'svcnum' => $recref->{catchall} } );
     return "Unknown catchall" unless $svc_acct;
   }
 
-  $self->ut_textn('purpose')
+  $self->ut_alphan('suffix')
+    or $self->ut_foreign_keyn('registrarnum', 'registrar', 'registrarnum')
+    or $self->ut_textn('registrarkey')
+    or $self->ut_numbern('setup_date')
+    or $self->ut_numbern('renewal_interval')
+    or $self->ut_numbern('expiration_date')
+    or $self->ut_textn('purpose')
     or $self->SUPER::check;
 
 }
     or $self->SUPER::check;
 
 }
@@ -402,7 +425,7 @@ sub catchall_svc_acct {
 
 sub whois {
   #$whois_hack or new Net::Whois::Domain $_[0]->domain;
 
 sub whois {
   #$whois_hack or new Net::Whois::Domain $_[0]->domain;
-  $whois_hack or die "whois_hack not set...\n";
+  #$whois_hack or die "whois_hack not set...\n";
 }
 
 =item _whois
 }
 
 =item _whois
index 14eab7e..5aaee48 100644 (file)
@@ -1,16 +1,11 @@
 package FS::svc_external;
 
 use strict;
 package FS::svc_external;
 
 use strict;
-use vars qw(@ISA); # $conf
-use FS::UID;
-#use FS::Record qw( qsearch qsearchs dbh);
-use FS::svc_Common;
+use vars qw(@ISA);
+use FS::Conf;
+use FS::svc_External_Common;
 
 
-@ISA = qw( FS::svc_Common );
-
-#FS::UID::install_callback( sub {
-#  $conf = new FS::Conf;
-#};
+@ISA = qw( FS::svc_External_Common );
 
 =head1 NAME
 
 
 =head1 NAME
 
@@ -39,9 +34,9 @@ FS::svc_external - Object methods for svc_external records
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
-An FS::svc_external object represents a externally tracked service.
-FS::svc_external inherits from FS::svc_Common.  The following fields are
-currently supported:
+An FS::svc_external object represents a generic externally tracked service.
+FS::svc_external inherits from FS::svc_External_Common (and FS::svc_Common).
+The following fields are currently supported:
 
 =over 4
 
 
 =over 4
 
@@ -67,8 +62,31 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
 
 =cut
 
+sub table_info {
+  {
+    'name' => 'External service',
+    'sorts' => 'id',
+    'display_weight' => 90,
+    'cancel_weight'  => 10,
+    'fields' => {
+    },
+  };
+}
+
 sub table { 'svc_external'; }
 
 sub table { 'svc_external'; }
 
+# oh!  this should be moved to svc_artera_turbo or something now
+sub label {
+  my $self = shift;
+  my $conf = new FS::Conf;
+  if ( $conf->config('svc_external-display_type') eq 'artera_turbo' ) {
+    sprintf('%010d', $self->id). '-'.
+      substr('0000000000'.uc($self->title), -10);
+  } else {
+    $self->SUPER::label;
+  }
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this external service to the database.  If there is an error, returns the
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this external service to the database.  If there is an error, returns the
@@ -149,21 +167,15 @@ and replace methods.
 
 =cut
 
 
 =cut
 
-sub check {
-  my $self = shift;
-
-  my $x = $self->setfixed;
-  return $x unless ref($x);
-  my $part_svc = $x;
-
-  my $error = 
-    $self->ut_numbern('svcnum')
-    || $self->ut_numbern('id')
-    || $self->ut_textn('title')
-  ;
-
-  $self->SUPER::check;
-}
+#sub check {
+#  my $self = shift;
+#  my $error;
+#
+#  $error = $self->SUPER::delete;
+#  return $error if $error;
+#
+#  '';
+#}
 
 =back
 
 
 =back
 
@@ -171,8 +183,8 @@ sub check {
 
 =head1 SEE ALSO
 
 
 =head1 SEE ALSO
 
-L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>, L<FS::part_svc>,
-L<FS::cust_pkg>, schema.html from the base documentation.
+L<FS::svc_External_Common>, L<FS::svc_Common>, L<FS::Record>, L<FS::cust_svc>,
+L<FS::part_svc>, L<FS::cust_pkg>, schema.html from the base documentation.
 
 =cut
 
 
 =cut
 
index ab24d32..91e251f 100644 (file)
@@ -66,8 +66,67 @@ database, see L<"insert">.
 
 =cut
 
 
 =cut
 
+
+sub table_info {
+  {
+    'name' => 'Forward',
+    'name_plural' => 'Mail forwards',
+    'display_weight' => 30,
+    'cancel_weight'  => 30,
+    'fields' => {
+        'srcsvc'    => 'service from which mail is to be forwarded',
+        'dstsvc'    => 'service to which mail is to be forwarded',
+        'dst'       => 'someone@another.domain.com to use when dstsvc is 0',
+    },
+  };
+}
+
 sub table { 'svc_forward'; }
 
 sub table { 'svc_forward'; }
 
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+  my( $class, $string ) = @_;
+  $class->search_sql_field('src', $string);
+}
+
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns a text string representing this forward.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+  my $self = shift;
+  my $tag = '';
+
+  if ( $self->srcsvc ) {
+    my $svc_acct = $self->srcsvc_acct(@_);
+    $tag = $svc_acct->email(@_);
+  } else {
+    $tag = $self->src;
+  }
+
+  $tag .= ' -> ';
+
+  if ( $self->dstsvc ) {
+    my $svc_acct = $self->dstsvc_acct(@_);
+    $tag .= $svc_acct->email(@_);
+  } else {
+    $tag .= $self->dst;
+  }
+
+  $tag;
+}
+
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this mail forwarding alias to the database.  If there is an error, returns
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this mail forwarding alias to the database.  If there is an error, returns
index fca3369..8e39b9f 100644 (file)
@@ -63,9 +63,51 @@ points to.  You can ask the object for a copy with the I<hash> method.
 =cut
 
 # the new method can be inherited from FS::Record, if a table method is defined
 =cut
 
 # the new method can be inherited from FS::Record, if a table method is defined
+#
+sub table_info {
+  {
+    'name' => 'Phone number',
+    'sorts' => 'phonenum',
+    'display_weight' => 60,
+    'cancel_weight'  => 80,
+    'fields' => {
+        'countrycode' => { label => 'Country code',
+                           type  => 'text',
+                           disable_inventory => 1,
+                         },
+        'phonenum'    => 'Phone number',
+        'pin'         => { label => 'Personal Identification Number',
+                           type  => 'text',
+                           disable_inventory => 1,
+                         },
+    },
+  };
+}
 
 sub table { 'svc_phone'; }
 
 
 sub table { 'svc_phone'; }
 
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+sub search_sql {
+  my( $class, $string ) = @_;
+  $class->search_sql_field('phonenum', $string );
+}
+
+=item label
+
+Returns the phone number.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->phonenum; #XXX format it better
+}
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
index 7c7032f..066719b 100644 (file)
@@ -72,8 +72,33 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
 
 =cut
 
+sub table_info {
+  {
+    'name' => 'Hosting',
+    'name_plural' => 'Virtual hosting services',
+    'display_weight' => 40,
+    'cancel_weight'  => 20,
+    'fields' => {
+    },
+  };
+};
+
 sub table { 'svc_www'; }
 
 sub table { 'svc_www'; }
 
+=item label [ END_TIMESTAMP [ START_TIMESTAMP ] ]
+
+Returns the zone name for this virtual host.
+
+END_TIMESTAMP and START_TIMESTAMP can optionally be passed when dealing with
+history records.
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->domain_record(@_)->zone;
+}
+
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this record to the database.  If there is an error, returns the error,
 =item insert [ , OPTION => VALUE ... ]
 
 Adds this record to the database.  If there is an error, returns the error,
index 906cc9c..c5c40c6 100644 (file)
@@ -355,6 +355,12 @@ FS/cust_bill_pay_pkg.pm
 t/cust_bill_pay_pkg.t
 FS/cust_credit_bill_pkg.pm
 t/cust_credit_bill_pkg.t
 t/cust_bill_pay_pkg.t
 FS/cust_credit_bill_pkg.pm
 t/cust_credit_bill_pkg.t
+FS/registrar.pm
+t/registrar.t
+FS/svc_External_Common.pm
+t/svc_External_Common.t
+FS/svc_Parent_Mixin.pm
+t/svc_Parent_Mixin.t
 FS/cust_main_note.pm
 t/cust_main_note.t
 FS/cust_pkg_reason.pm
 FS/cust_main_note.pm
 t/cust_main_note.t
 FS/cust_pkg_reason.pm
diff --git a/FS/t/registrar.t b/FS/t/registrar.t
new file mode 100644 (file)
index 0000000..a6ba134
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::registrar;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_External_Common.t b/FS/t/svc_External_Common.t
new file mode 100644 (file)
index 0000000..a0b2ea2
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_External_Common;
+$loaded=1;
+print "ok 1\n";
diff --git a/FS/t/svc_Parent_Mixin.t b/FS/t/svc_Parent_Mixin.t
new file mode 100644 (file)
index 0000000..ed9923f
--- /dev/null
@@ -0,0 +1,5 @@
+BEGIN { $| = 1; print "1..1\n" }
+END {print "not ok 1\n" unless $loaded;}
+use FS::svc_Parent_Mixin;
+$loaded=1;
+print "ok 1\n";
index 7f7ef4b..47dcbe6 100644 (file)
@@ -59,6 +59,60 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 sub table { 'table_name'; }
 
 
 sub table { 'table_name'; }
 
+sub table_info {
+  {
+    'name' => 'Example',
+    'name_plural' => 'Example services', #optional,
+    'longname_plural' => 'Example services', #optional
+    'sorts' => 'svcnum', # optional sort field (or arrayref of sort fields, main first)
+    'display_weight' => 100,
+    'cancel_weight'  => 100,
+    'fields' => {
+      'field'         => 'Description',
+      'another_field' => { 
+                           'label'     => 'Description',
+                          'def_label' => 'Description for service definitions',
+                          'type'      => 'text',
+                          'disable_default'   => 1, #disable switches
+                          'disable_fixed'     => 1, #
+                          'disable_inventory' => 1, #
+                        },
+      'foreign_key'   => { 
+                           'label'        => 'Description',
+                          'def_label'    => 'Description for service defs',
+                          'type'         => 'select',
+                          'select_table' => 'foreign_table',
+                          'select_key'   => 'key_field_in_table',
+                          'select_label' => 'label_field_in_table',
+                        },
+
+    },
+  };
+}
+
+=item search_sql STRING
+
+Class method which returns an SQL fragment to search for the given string.
+
+=cut
+
+#or something more complicated if necessary
+sub search_sql {
+  my($class, $string) = @_;
+  $class->search_sql_field('search_field', $string);
+}
+
+=item label
+
+Returns a meaningful identifier for this example
+
+=cut
+
+sub label {
+  my $self = shift;
+  $self->label_field; #or something more complicated if necessary
+}
+
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
 =item insert
 
 Adds this record to the database.  If there is an error, returns the error,
index 7953935..6198a1a 100755 (executable)
@@ -72,21 +72,33 @@ function part_export_areyousure(href) {
 %>
 % $cgi->param('showdisabled', ( 1 ^ $cgi->param('showdisabled') ) ); 
 
 %>
 % $cgi->param('showdisabled', ( 1 ^ $cgi->param('showdisabled') ) ); 
 
-<% table() %>
+<% include('/elements/table-grid.html') %>
+% my $bgcolor1 = '#eeeeee';
+%   my $bgcolor2 = '#ffffff';
+%   my $bgcolor = '';
+
   <TR>
   <TR>
-    <TH><A HREF="<% do { $cgi->param('orderby', 'svcpart'); $cgi->self_url } %>">#</A></TH>
-% if ( $cgi->param('showdisabled') ) { 
 
 
-      <TH>Status</TH>
+    <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'svcpart'); $cgi->self_url } %>">#</A></TH>
+
+% if ( $cgi->param('showdisabled') ) { 
+      <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
 % } 
 
 % } 
 
-    <TH><A HREF="<% do { $cgi->param('orderby', 'svc'); $cgi->self_url; } %>">Service</A></TH>
-    <TH>Table</TH>
-    <TH><A HREF="<% do { $cgi->param('orderby', 'active'); $cgi->self_url; } %>"><FONT SIZE=-1>Customer<BR>Services</FONT></A></TH>
-    <TH>Export</TH>
-    <TH>Field</TH>
-    <TH COLSPAN=2>Modifier</TH>
+    <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'svc'); $cgi->self_url; } %>">Service</A></TH>
+
+    <TH CLASS="grid" BGCOLOR="#cccccc">Table</TH>
+
+    <TH CLASS="grid" BGCOLOR="#cccccc"><A HREF="<% do { $cgi->param('orderby', 'active'); $cgi->self_url; } %>"><FONT SIZE=-1>Customer<BR>Services</FONT></A></TH>
+
+    <TH CLASS="grid" BGCOLOR="#cccccc">Export</TH>
+
+    <TH CLASS="grid" BGCOLOR="#cccccc">Field</TH>
+
+    <TH COLSPAN=2 CLASS="grid" BGCOLOR="#cccccc">Modifier</TH>
+
   </TR>
   </TR>
+
 % foreach my $part_svc ( @part_svc ) {
 %     my $svcdb = $part_svc->svcdb;
 %     my $svc_x = "FS::$svcdb"->new( { svcpart => $part_svc->svcpart } );
 % foreach my $part_svc ( @part_svc ) {
 %     my $svcdb = $part_svc->svcdb;
 %     my $svc_x = "FS::$svcdb"->new( { svcpart => $part_svc->svcpart } );
@@ -99,14 +111,21 @@ function part_export_areyousure(href) {
 %     my $rowspan = scalar(@fields) || 1;
 %     my $url = "${p}edit/part_svc.cgi?". $part_svc->svcpart;
 %
 %     my $rowspan = scalar(@fields) || 1;
 %     my $url = "${p}edit/part_svc.cgi?". $part_svc->svcpart;
 %
+%     if ( $bgcolor eq $bgcolor1 ) {
+%       $bgcolor = $bgcolor2;
+%     } else {
+%       $bgcolor = $bgcolor1;
+%     }
 
 
   <TR>
 
 
   <TR>
-    <TD ROWSPAN=<% $rowspan %>><A HREF="<% $url %>">
-      <% $part_svc->svcpart %></A></TD>
-% if ( $cgi->param('showdisabled') ) { 
 
 
-    <TD ROWSPAN=<% $rowspan %>>
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <A HREF="<% $url %>"><% $part_svc->svcpart %></A>
+    </TD>
+
+% if ( $cgi->param('showdisabled') ) { 
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
       <% $part_svc->disabled
             ? '<FONT COLOR="#FF0000"><B>Disabled</B></FONT>'
             : '<FONT COLOR="#00CC00"><B>Enabled</B></FONT>'
       <% $part_svc->disabled
             ? '<FONT COLOR="#FF0000"><B>Disabled</B></FONT>'
             : '<FONT COLOR="#00CC00"><B>Enabled</B></FONT>'
@@ -114,19 +133,23 @@ function part_export_areyousure(href) {
     </TD>
 % } 
 
     </TD>
 % } 
 
-    <TD ROWSPAN=<% $rowspan %>><A HREF="<% $url %>">
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
       <% $part_svc->svc %></A></TD>
       <% $part_svc->svc %></A></TD>
-    <TD ROWSPAN=<% $rowspan %>>
+
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
       <% $svcdb %></TD>
       <% $svcdb %></TD>
-    <TD ROWSPAN=<% $rowspan %>>
-      <FONT COLOR="#00CC00"><B><% $num_active_cust_svc{$part_svc->svcpart} %></B></FONT>&nbsp;<A HREF="<%$p%>search/<% $svcdb %>.cgi?svcpart=<% $part_svc->svcpart %>">active</A>
-% if ( $num_active_cust_svc{$part_svc->svcpart} ) { 
 
 
+    <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+      <FONT COLOR="#00CC00"><B><% $num_active_cust_svc{$part_svc->svcpart} %></B></FONT>&nbsp;<% $num_active_cust_svc{$part_svc->svcpart} ? FS::UI::Web::svc_url( 'ahref' => 1, 'm' => $m, 'action' => 'search', 'part_svc' => $part_svc, 'query' => "svcpart=". $part_svc->svcpart ) : '<A NAME="zero">' %>active</A>
+
+% if ( $num_active_cust_svc{$part_svc->svcpart} ) { 
         <BR><FONT SIZE="-1">[ <A HREF="<%$p%>edit/bulk-cust_svc.html?svcpart=<% $part_svc->svcpart %>">change</A> ]</FONT>
 % } 
 
     </TD>
         <BR><FONT SIZE="-1">[ <A HREF="<%$p%>edit/bulk-cust_svc.html?svcpart=<% $part_svc->svcpart %>">change</A> ]</FONT>
 % } 
 
     </TD>
-    <TD ROWSPAN=<% $rowspan %>><% itable() %>
+
+    <TD ROWSPAN=<% $rowspan %> CLASS="inv" BGCOLOR="<% $bgcolor %>">
+      <TABLE CLASS="inv">
 %
 %#  my @part_export =
 %map { qsearchs('part_export', { exportnum => $_->exportnum } ) } qsearch('export_svc', { svcpart => $part_svc->svcpart } ) ;
 %
 %#  my @part_export =
 %map { qsearchs('part_export', { exportnum => $_->exportnum } ) } qsearch('export_svc', { svcpart => $part_svc->svcpart } ) ;
@@ -136,21 +159,30 @@ function part_export_areyousure(href) {
 %  ) {
 %
 
 %  ) {
 %
 
-      <TR>
-        <TD><A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>"><% $part_export->exportnum %>:&nbsp;<% $part_export->exporttype %>&nbsp;to&nbsp;<% $part_export->machine %></A></TD></TR>
+        <TR>
+          <TD><A HREF="<% $p %>edit/part_export.cgi?<% $part_export->exportnum %>"><% $part_export->exportnum %>:&nbsp;<% $part_export->exporttype %>&nbsp;to&nbsp;<% $part_export->machine %></A></TD>
+       </TR>
 %  } 
 
 %  } 
 
-      </TABLE></TD>
-%   my($n1)='';
+      </TABLE>
+    </TD>
+
+%     unless ( @fields ) {
+%       for ( 1..3 ) {  
+         <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"</TD>
+%       }
+%     }
+%   
+%     my($n1)='';
 %     foreach my $field ( @fields ) {
 %       my $flag = $part_svc->part_svc_column($field)->columnflag;
 %
 
      <% $n1 %>
 %     foreach my $field ( @fields ) {
 %       my $flag = $part_svc->part_svc_column($field)->columnflag;
 %
 
      <% $n1 %>
-     <TD><% $field %></TD>
-     <TD><% $flag{$flag} %></TD>
+     <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $field %></TD>
+     <TD CLASS="grid" BGCOLOR="<% $bgcolor %>"><% $flag{$flag} %></TD>
 
 
-     <TD>
+     <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
 % my $value = $part_svc->part_svc_column($field)->columnvalue;
 %          if ( $flag =~ /^[MA]$/ ) { 
 %            $inventory_class{$value}
 % my $value = $part_svc->part_svc_column($field)->columnvalue;
 %          if ( $flag =~ /^[MA]$/ ) { 
 %            $inventory_class{$value}
index c2ea22f..17c5ad3 100644 (file)
@@ -53,6 +53,9 @@
 %  #                        # ...
 %  #                        "html_string";
 %  #                      },
 %  #                        # ...
 %  #                        "html_string";
 %  #                      },
+%  #
+%  # # overrides default popurl(1)."process/$table.html"
+%  # 'post_url' => popurl(1).'process/something', 
 %
 %  my(%opt) = @_;
 %
 %
 %  my(%opt) = @_;
 %
 %
 %  } elsif ( $cgi->keywords || $cgi->param($pkey) ) { #editing
 %
 %
 %  } elsif ( $cgi->keywords || $cgi->param($pkey) ) { #editing
 %
-%    my( $query ) = $cgi->keywords;
-%    $query = $cgi->param($pkey) unless $query;
-%    $query =~ /^(\d+)$/;
+%    my $value;
+%    if ( $cgi->param($pkey) ) {
+%      $value = $cgi->param($pkey)
+%    } else { 
+%      my( $query ) = $cgi->keywords;
+%      $value = $query;
+%    }
+%    $value =~ /^(\d+)$/ or die "unparsable $pkey";
 %    $object = qsearchs( $table, { $pkey => $1 } );
 %    warn "$table $pkey => $1"
 %      if $opt{'debug'};
 %    $object = qsearchs( $table, { $pkey => $1 } );
 %    warn "$table $pkey => $1"
 %      if $opt{'debug'};
   <BR><BR>
 % } 
 
   <BR><BR>
 % } 
 
+% my $url = $opt{'post_url'} || popurl(1)."process/$table.html";
 
 
-<FORM ACTION="<% popurl(1) %>process/<% $table %>.html" METHOD=POST>
+<FORM ACTION="<% $url %>" METHOD=POST>
+<INPUT TYPE="hidden" NAME="svcdb" VALUE="<% $table %>">
 <INPUT TYPE="hidden" NAME="<% $pkey %>" VALUE="<% $object->$pkey() %>">
 <% ( $opt{labels} && exists $opt{labels}->{$pkey} )
       ? $opt{labels}->{$pkey}
 <INPUT TYPE="hidden" NAME="<% $pkey %>" VALUE="<% $object->$pkey() %>">
 <% ( $opt{labels} && exists $opt{labels}->{$pkey} )
       ? $opt{labels}->{$pkey}
index da59cc9..1fd66c2 100644 (file)
@@ -1,19 +1,21 @@
 %
 %
-%
 %  my %opt = @_;
 %
 %  #my( $svcnum, $pkgnum, $svcpart, $part_svc );
 %  my( $pkgnum, $svcpart, $part_svc );
 %
 %  #get & untaint pkgnum & svcpart
 %  my %opt = @_;
 %
 %  #my( $svcnum, $pkgnum, $svcpart, $part_svc );
 %  my( $pkgnum, $svcpart, $part_svc );
 %
 %  #get & untaint pkgnum & svcpart
-%  my($query) = $cgi->keywords; #they're not proper cgi params
-%  if ( $query =~ /^pkgnum(\d+)-svcpart(\d+)$/ ) {
-%    $pkgnum  = $1;
-%    $svcpart = $2;
-%    $cgi->delete_all(); #so the standard edit.html treats this correctly as new
+%  if ( ! $cgi->param('error')
+%       && $cgi->param('pkgnum') && $cgi->param('svcpart')
+%     )
+%  {
+%    $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%    $pkgnum = $1;
+%    $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%    $svcpart = $1;
+%    $cgi->delete_all(); #so edit.html treats this correctly as new??
 %  }
 %
 %  }
 %
-%
 <% include( 'edit.html',
 
                  'menubar' => [],
 <% include( 'edit.html',
 
                  'menubar' => [],
index cc9145f..ba152db 100755 (executable)
@@ -55,159 +55,37 @@ For the selected table, you can give fields default or fixed (unchangable)
 values, or select an inventory class to manually or automatically fill in
 that field.
 <BR><BR>
 values, or select an inventory class to manually or automatically fill in
 that field.
 <BR><BR>
-%
-%
-%#these might belong somewhere else for other user interfaces 
-%#pry need to eventually create stuff that's shared amount UIs
-%my $conf = new FS::Conf;
-%my %defs = (
-%
-%  'svc_acct' => {
-%    'dir'       => 'Home directory',
-%    'uid'       => 'UID (set to fixed and blank for no UIDs)',
-%    'slipip'    => 'IP address',
-%#    'popnum'    => qq!<A HREF="$p/browse/svc_acct_pop.cgi/">POP number</A>!,
-%    'popnum'    => {
-%                     desc => 'Access number',
-%                     type => 'select',
-%                     select_table => 'svc_acct_pop',
-%                     select_key   => 'popnum',
-%                     select_label => 'city',
-%                     disable_select => 1,
-%                   },
-%    'username'  => {
-%                     desc => 'Username',
-%                     type => 'text',
-%                     disable_default => 1,
-%                     disable_fixed => 1,
-%                     disable_select => 1,
-%                   },
-%    'quota'     => { 
-%                     desc => '',
-%                     type => 'text',
-%                     disable_inventory => 1,
-%                     disable_select => 1,
-%                   },
-%    '_password' => 'Password',
-%    'gid'       => 'GID (when blank, defaults to UID)',
-%    'shell'     => {
-%                     #desc =>'Shell (all service definitions should have a default or fixed shell that is present in the <b>shells</b> configuration file, set to blank for no shell tracking)',
-%                     desc =>'Shell ( set to blank for no shell tracking)',
-%                     type =>'select',
-%                     select_list => [ $conf->config('shells') ],
-%                     disable_inventory => 1,
-%                     disable_select => 1,
-%                   },
-%    'finger'    => 'Real name (GECOS)',
-%    'domsvc'    => {
-%                     desc =>'svcnum from svc_domain',
-%                     type =>'select',
-%                     select_table => 'svc_domain',
-%                     select_key   => 'svcnum',
-%                     select_label => 'domain',
-%                     disable_inventory => 1,
-%                     disable_select => 1,
-%                   },
-%    'usergroup' => {
-%                     desc =>'RADIUS groups',
-%                     type =>'radius_usergroup_selector',
-%                     disable_select => 1,
-%                     disable_inventory => 1,
-%                   },
-%    'seconds'   => { desc => '',
-%                     type => 'text',
-%                     disable_inventory => 1,
-%                     disable_select => 1,
-%                   },
-%  },
-%
-%  'svc_domain' => {
-%    'domain'    => 'Domain',
-%  },
-%
-%  'svc_forward' => {
-%    'srcsvc'    => 'service from which mail is to be forwarded',
-%    'dstsvc'    => 'service to which mail is to be forwarded',
-%    'dst'       => 'someone@another.domain.com to use when dstsvc is 0',
-%  },
-%
-%#  'svc_charge' => {
-%#    'amount'    => 'amount',
-%#  },
-%#  'svc_wo' => {
-%#    'worker'    => 'Worker',
-%#    '_date'      => 'Date',
-%#  },
-%
-%  'svc_www' => {
-%    #'recnum' => '',
-%    #'usersvc' => '',
-%  },
-%
-%  'svc_broadband' => {
-%    'speed_down' => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
-%    'speed_up' => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
-%    'ip_addr' => 'IP address.  Leave blank for automatic assignment.',
-%    'blocknum' => 'Address block.',
-%  },
-%
-%  'svc_phone' => {
-%    'countrycode' => { desc => 'Country code',
-%                       type => 'text',
-%                       disable_inventory => 1,
-%                       disable_select => 1,
-%                     },
-%    'phonenum'    => 'Phone number',
-%    'pin'         => { desc => 'Personal Identification Number',
-%                       type => 'text',
-%                       disable_inventory => 1,
-%                       disable_select => 1,
-%                     },
-%  },
-%
-%  'svc_external' => {
-%    #'id' => '',
-%    #'title' => '',
-%  },
-%
-%);
-%
-%  my %vfields;
-%  foreach my $svcdb (grep dbdef->table($_), keys %defs ) {
-%    my $self = "FS::$svcdb"->new;
-%    $vfields{$svcdb} = {};
-%    foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
-%      my $pvf = $self->pvf($field);
-%      my @list = $pvf->list;
-%      if (scalar @list) {
-%        $defs{$svcdb}->{$field} = { desc        => $pvf->label,
-%                                    type        => 'select',
-%                                    select_list => \@list };
-%      } else {
-%        $defs{$svcdb}->{$field} = $pvf->label;
-%      } #endif
-%      $vfields{$svcdb}->{$field} = $pvf;
-%      warn "\$vfields{$svcdb}->{$field} = $pvf";
-%    } #next $field
-%  } #next $svcdb
+
+% #YUCK.  false laziness w/part_svc.pm.  go away virtual fields, please
+% my %vfields;
+% foreach my $svcdb ( FS::part_svc->svc_tables() ) {
+%   eval "use FS::$svcdb;";
+%   my $self = "FS::$svcdb"->new;
+%   $vfields{$svcdb} = {};
+%   foreach my $field ($self->virtual_fields) { # svc_Common::virtual_fields with a null svcpart returns all of them
+%     my $pvf = $self->pvf($field);
+%     $vfields{$svcdb}->{$field} = $pvf;
+%     #warn "\$vfields{$svcdb}->{$field} = $pvf";
+%   } #next $field
+% } #next $svcdb
 %
 %  #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
 %  # and generalize the subs
 %  # condition sub is tested to see whether to disable display of this choice
 %  # params: ( $def, $layer, $field )  (see SUB below)
 %  my $inv_sub = sub {
 %
 %  #code duplication w/ edit/part_svc.cgi, should move this hash to part_svc.pm
 %  # and generalize the subs
 %  # condition sub is tested to see whether to disable display of this choice
 %  # params: ( $def, $layer, $field )  (see SUB below)
 %  my $inv_sub = sub {
-%    ref($_[0]) && (    $_[0]->{disable_inventory} 
-%                    || $_[0]->{'type'} ne 'text'  )
-%  };
+%                      $_[0]->{disable_inventory}
+%                        || $_[0]->{'type'} ne 'text'
+%                    };
 %  tie my %flag, 'Tie::IxHash',
 %    ''  => { 'desc' => 'No default', },
 %    'D' => { 'desc' => 'Default',
 %             'condition' =>
 %  tie my %flag, 'Tie::IxHash',
 %    ''  => { 'desc' => 'No default', },
 %    'D' => { 'desc' => 'Default',
 %             'condition' =>
-%               sub { ref($_[0]) && $_[0]->{disable_default} }, 
+%               sub { $_[0]->{disable_default} }, 
 %           },
 %    'F' => { 'desc' => 'Fixed (unchangeable)',
 %             'condition' =>
 %           },
 %    'F' => { 'desc' => 'Fixed (unchangeable)',
 %             'condition' =>
-%               sub { ref($_[0]) && $_[0]->{disable_fixed} }, 
+%               sub { $_[0]->{disable_fixed} }, 
 %           },
 %    'S' => { 'desc' => 'Selectable Choice',
 %             'condition' =>
 %           },
 %    'S' => { 'desc' => 'Selectable Choice',
 %             'condition' =>
@@ -229,7 +107,7 @@ that field.
 %  
 %  my @dbs = $hashref->{svcdb}
 %             ? ( $hashref->{svcdb} )
 %  
 %  my @dbs = $hashref->{svcdb}
 %             ? ( $hashref->{svcdb} )
-%             : qw( svc_acct svc_domain svc_forward svc_www svc_broadband svc_phone svc_external );
+%             : FS::part_svc->svc_tables();
 %
 %  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
 %  my $widget = new HTML::Widgets::SelectLayers(
 %
 %  tie my %svcdb, 'Tie::IxHash', map { $_=>$_ } grep dbdef->table($_), @dbs;
 %  my $widget = new HTML::Widgets::SelectLayers(
@@ -291,8 +169,9 @@ that field.
 %        my $part_svc_column = $part_svc->part_svc_column($field);
 %        my $value = $part_svc_column->columnvalue;
 %        my $flag = $part_svc_column->columnflag;
 %        my $part_svc_column = $part_svc->part_svc_column($field);
 %        my $value = $part_svc_column->columnvalue;
 %        my $flag = $part_svc_column->columnflag;
-%        my $def = $defs{$layer}{$field};
-%        my $desc = ref($def) ? $def->{desc} : $def;
+%        #my $def = $defs{$layer}{$field};
+%        my $def = FS::part_svc->svc_table_fields($layer)->{$field};
+%        my $label = $def->{'def_label'} || $def->{'label'};
 %
 %        if ( $bgcolor eq $bgcolor1 ) {
 %          $bgcolor = $bgcolor2;
 %
 %        if ( $bgcolor eq $bgcolor1 ) {
 %          $bgcolor = $bgcolor2;
@@ -301,14 +180,13 @@ that field.
 %        }
 %        
 %        $html .= qq!<TR><TD CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!.
 %        }
 %        
 %        $html .= qq!<TR><TD CLASS="grid" BGCOLOR="$bgcolor" ALIGN="right">!.
-%                 $field;
-%        $html .= "- <FONT SIZE=-1>$desc</FONT>" if $desc;
-%        $html .=  "</TD>";
-%        $flag = '' if ref($def) && $def->{type} eq 'disabled';
+%                 ( $label || $field ).
+%                 "</TD>";
+%        $flag = '' if $def->{type} eq 'disabled';
 %
 %        $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!;
 %
 %
 %        $html .= qq!<TD CLASS="grid" BGCOLOR="$bgcolor">!;
 %
-%        if ( ref($def) && $def->{type} eq 'disabled' ) {
+%        if ( $def->{type} eq 'disabled' ) {
 %        
 %          $html .= 'No default';
 %
 %        
 %          $html .= 'No default';
 %
@@ -372,7 +250,7 @@ that field.
 %        my $disabled = $flag ? ''
 %                             : 'DISABLED STYLE="background-color: #dddddd"';
 %
 %        my $disabled = $flag ? ''
 %                             : 'DISABLED STYLE="background-color: #dddddd"';
 %
-%        if ( ! ref($def) || $def->{type} eq 'text' ) {
+%        if ( !$def->{type} || $def->{type} eq 'text' ) {
 %
 %          my $nodisplay = ' STYLE="display:none"';
 %          my $is_inv = ( $flag =~ /^[MA]$/ );
 %
 %          my $nodisplay = ' STYLE="display:none"';
 %          my $is_inv = ( $flag =~ /^[MA]$/ );
index 96d5687..4b1d2c8 100644 (file)
@@ -19,6 +19,8 @@
 %  # OR
 %  # 'redirect'    => 'view/table.cgi?', # value of primary key is appended
 %  #
 %  # OR
 %  # 'redirect'    => 'view/table.cgi?', # value of primary key is appended
 %  #
+%  # 'error_redirect' => popurl(2).'edit/table.cgi?', #query string appended
+%  #
 %  # 'edit_ext' => 'html', #defaults to 'html', you might want 'cgi' while the
 %  #                       #naming is still inconsistent
 %  # 
 %  # 'edit_ext' => 'html', #defaults to 'html', you might want 'cgi' while the
 %  #                       #naming is still inconsistent
 %  # 
@@ -78,7 +80,8 @@
 %  if ( $error ) {
 %    $cgi->param('error', $error);
 %    my $edit_ext = $opt{'edit_ext'} || 'html';
 %  if ( $error ) {
 %    $cgi->param('error', $error);
 %    my $edit_ext = $opt{'edit_ext'} || 'html';
-%    print $cgi->redirect(popurl(2). "$table.$edit_ext?". $cgi->query_string );
+%    my $url = $opt{'error_redirect'} || popurl(2)."$table.$edit_ext?";
+%    print $cgi->redirect($url. $cgi->query_string );
 %  } elsif ( $opt{'redirect'} ) {
 %    print $cgi->redirect( $opt{'redirect'}. $pkeyvalue );
 %  } else { 
 %  } elsif ( $opt{'redirect'} ) {
 %    print $cgi->redirect( $opt{'redirect'}. $pkeyvalue );
 %  } else { 
diff --git a/httemplate/edit/process/svc_Common.html b/httemplate/edit/process/svc_Common.html
new file mode 100644 (file)
index 0000000..f5c869a
--- /dev/null
@@ -0,0 +1,13 @@
+<%init>
+
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+my $table = $1;
+require "FS/$table.pm";
+
+</%init>
+<% include( 'elements/svc_Common.html',
+              'table'    => $table,
+             'redirect' => popurl(3)."view/svc_Common.html?svcdb=$table;svcnum=",
+             'error_redirect' => popurl(3)."edit/svc_Common.html?svcdb=$table;",
+         )
+%>
diff --git a/httemplate/edit/svc_Common.html b/httemplate/edit/svc_Common.html
new file mode 100644 (file)
index 0000000..6393f9e
--- /dev/null
@@ -0,0 +1,30 @@
+<%init>
+
+# false laziness w/view/svc_Common.html
+
+$cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unparsable svcdb";
+my $table = $1;
+require "FS/$table.pm";
+
+my %opt;
+if ( UNIVERSAL::can("FS::$table", 'table_info') ) {
+  $opt{'name'}   = "FS::$table"->table_info->{'name'};
+
+  my $fields = "FS::$table"->table_info->{'fields'};
+  my %labels = map { $_ => ( ref($fields->{$_})
+                               ? $fields->{$_}{'label'}
+                              : $fields->{$_}
+                          );
+                   }
+               keys %$fields;
+  $opt{'labels'} = \%labels;
+
+}
+
+</%init>
+<% include('elements/svc_Common.html',
+             'table'        => $table,
+            'post_url'     => popurl(1). "process/svc_Common.html",
+            %opt,
+         )
+%>
index f552967..f42c146 100755 (executable)
 %  die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
 %  @groups = $cgi->param('radius_usergroup');
 %
 %  die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
 %  @groups = $cgi->param('radius_usergroup');
 %
-%} else {
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
 %
 %
-%  my($query) = $cgi->keywords;
-%  if ( $query =~ /^(\d+)$/ ) { #editing
-%    $svcnum=$1;
-%    $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svcnum})
-%      or die "Unknown (svc_acct) svcnum!";
+%  $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%  $pkgnum = $1;
+%  $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%  $svcpart = $1;
 %
 %
-%    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-%      or die "Unknown (cust_svc) svcnum!";
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%    $pkgnum=$cust_svc->pkgnum;
-%    $svcpart=$cust_svc->svcpart;
+%    $svc_acct = new FS::svc_acct({svcpart => $svcpart}); 
 %
 %
-%    $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
-%    die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+%    $svcnum='';
 %
 %
-%    @groups = $svc_acct->radius_groups;
+%} else { #editing
 %
 %
-%  } else { #adding
+%  my($query) = $cgi->keywords;
+%  $query =~ /^(\d+)$/ or die "unparsable svcnum";
+%  $svcnum=$1;
+%  $svc_acct=qsearchs('svc_acct',{'svcnum'=>$svcnum})
+%    or die "Unknown (svc_acct) svcnum!";
 %
 %
-%    foreach $_ (split(/-/,$query)) {
-%      $pkgnum=$1 if /^pkgnum(\d+)$/;
-%      $svcpart=$1 if /^svcpart(\d+)$/;
-%    }
-%    $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
-%    die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+%  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+%    or die "Unknown (cust_svc) svcnum!";
 %
 %
-%    $svc_acct = new FS::svc_acct({svcpart => $svcpart}); 
+%  $pkgnum=$cust_svc->pkgnum;
+%  $svcpart=$cust_svc->svcpart;
 %
 %
-%    $svcnum='';
+%  $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
+%  die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
+%
+%  @groups = $svc_acct->radius_groups;
 %
 %
-%  }
 %}
 %
 %my( $cust_pkg, $cust_main ) = ( '', '' );
 %}
 %
 %my( $cust_pkg, $cust_main ) = ( '', '' );
index 6b95355..30eb631 100644 (file)
@@ -1,10 +1,6 @@
-<!-- mason kludge -->
-%
-%
 %# If it's stupid but it works, it's still stupid.
 %#  -Kristian
 %
 %# If it's stupid but it works, it's still stupid.
 %#  -Kristian
 %
-%
 %use HTML::Widgets::SelectLayers;
 %use Tie::IxHash;
 %
 %use HTML::Widgets::SelectLayers;
 %use Tie::IxHash;
 %
index 19e0e12..5ec074b 100755 (executable)
@@ -1,8 +1,7 @@
-%
-%
 %my($svcnum, $pkgnum, $svcpart, $kludge_action, $purpose, $part_svc,
 %   $svc_domain);
 %if ( $cgi->param('error') ) {
 %my($svcnum, $pkgnum, $svcpart, $kludge_action, $purpose, $part_svc,
 %   $svc_domain);
 %if ( $cgi->param('error') ) {
+%
 %  $svc_domain = new FS::svc_domain ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_domain')
 %  } );
 %  $svc_domain = new FS::svc_domain ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_domain')
 %  } );
 %  $purpose = $cgi->param('purpose');
 %  $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
 %  die "No part_svc entry!" unless $part_svc;
 %  $purpose = $cgi->param('purpose');
 %  $part_svc = qsearchs('part_svc', { 'svcpart' => $svcpart } );
 %  die "No part_svc entry!" unless $part_svc;
-%} else {
-%  $kludge_action = '';
-%  $purpose = '';
-%  my($query) = $cgi->keywords;
-%  if ( $query =~ /^(\d+)$/ ) { #editing
-%    $svcnum=$1;
-%    $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
-%      or die "Unknown (svc_domain) svcnum!";
 %
 %
-%    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-%      or die "Unknown (cust_svc) svcnum!";
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+%  $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%  $pkgnum = $1;
+%  $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%  $svcpart = $1;
 %
 %
-%    $pkgnum=$cust_svc->pkgnum;
-%    $svcpart=$cust_svc->svcpart;
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $svc_domain = new FS::svc_domain({});
 %
 %
-%  } else { #adding
+%  $svcnum='';
 %
 %
-%    $svc_domain = new FS::svc_domain({});
-%  
-%    foreach $_ (split(/-/,$query)) {
-%      $pkgnum=$1 if /^pkgnum(\d+)$/;
-%      $svcpart=$1 if /^svcpart(\d+)$/;
-%    }
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $svc_domain->set_default_and_fixed;
 %
 %
-%    $svcnum='';
+%} else { #editing
 %
 %
-%    $svc_domain->set_default_and_fixed;
+%  $kludge_action = '';
+%  $purpose = '';
+%  my($query) = $cgi->keywords;
+%  $query =~ /^(\d+)$/ or die "unparsable svcnum";
+%  $svcnum=$1;
+%  $svc_domain=qsearchs('svc_domain',{'svcnum'=>$svcnum})
+%    or die "Unknown (svc_domain) svcnum!";
 %
 %
-%  }
+%  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+%    or die "Unknown (cust_svc) svcnum!";
+%
+%  $pkgnum=$cust_svc->pkgnum;
+%  $svcpart=$cust_svc->svcpart;
+%
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %}
 %my $action = $svcnum ? 'Edit' : 'Add';
 %
 %}
 %my $action = $svcnum ? 'Edit' : 'Add';
index 1230340..393e71c 100644 (file)
@@ -1,8 +1,6 @@
-<!-- mason kludge -->
-%
-%
 %my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_external );
 %if ( $cgi->param('error') ) {
 %my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_external );
 %if ( $cgi->param('error') ) {
+%
 %  $svc_external = new FS::svc_external ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_external')
 %  } );
 %  $svc_external = new FS::svc_external ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_external')
 %  } );
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
-%} else {
-%  my($query) = $cgi->keywords;
-%  if ( $query =~ /^(\d+)$/ ) { #editing
-%    $svcnum=$1;
-%    $svc_external=qsearchs('svc_external',{'svcnum'=>$svcnum})
-%      or die "Unknown (svc_external) svcnum!";
 %
 %
-%    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-%      or die "Unknown (cust_svc) svcnum!";
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
 %
 %
-%    $pkgnum=$cust_svc->pkgnum;
-%    $svcpart=$cust_svc->svcpart;
-%  
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%  $pkgnum = $1;
+%  $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%  $svcpart = $1;
 %
 %
-%  } else { #adding
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
-%      $pkgnum=$1 if /^pkgnum(\d+)$/;
-%      $svcpart=$1 if /^svcpart(\d+)$/;
-%    }
-%    $svc_external = new FS::svc_external { svcpart => $svcpart };
+%  $svc_external = new FS::svc_external { svcpart => $svcpart };
 %
 %
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $svcnum='';
 %
 %
-%    $svcnum='';
+%  $svc_external->set_default_and_fixed;
 %
 %
-%    $svc_external->set_default_and_fixed;
+%} else { #adding
+%
+%  my($query) = $cgi->keywords;
+%  $query =~ /^(\d+)$/ or die "unparsable svcnum";
+%  $svcnum=$1;
+%  $svc_external=qsearchs('svc_external',{'svcnum'=>$svcnum})
+%    or die "Unknown (svc_external) svcnum!";
+%
+%  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+%    or die "Unknown (cust_svc) svcnum!";
+%
+%  $pkgnum=$cust_svc->pkgnum;
+%  $svcpart=$cust_svc->svcpart;
+%  
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%  }
 %}
 %my $action = $svc_external->svcnum ? 'Edit' : 'Add';
 %
 %}
 %my $action = $svc_external->svcnum ? 'Edit' : 'Add';
 %
index 73b32dc..ef08ffc 100755 (executable)
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
-%} else {
 %
 %
-%  my($query) = $cgi->keywords;
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
+%
+%  $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%  $pkgnum = $1;
+%  $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%  $svcpart = $1;
 %
 %
-%  if ( $query =~ /^(\d+)$/ ) { #editing
-%    $svcnum=$1;
-%    $svc_forward=qsearchs('svc_forward',{'svcnum'=>$svcnum})
-%      or die "Unknown (svc_forward) svcnum!";
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-%      or die "Unknown (cust_svc) svcnum!";
+%  $svc_forward = new FS::svc_forward({});
 %
 %
-%    $pkgnum=$cust_svc->pkgnum;
-%    $svcpart=$cust_svc->svcpart;
-%  
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $svcnum='';
 %
 %
-%  } else { #adding
+%  $svc_forward->set_default_and_fixed;
 %
 %
-%    $svc_forward = new FS::svc_forward({});
+%} else { #editing
 %
 %
-%    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
-%      $pkgnum=$1 if /^pkgnum(\d+)$/;
-%      $svcpart=$1 if /^svcpart(\d+)$/;
-%    }
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  my($query) = $cgi->keywords;
 %
 %
-%    $svcnum='';
+%  $query =~ /^(\d+)$/ or die "unparsable svcnum";
+%  $svcnum=$1;
+%  $svc_forward=qsearchs('svc_forward',{'svcnum'=>$svcnum})
+%    or die "Unknown (svc_forward) svcnum!";
 %
 %
-%    $svc_forward->set_default_and_fixed;
-%  }
+%  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+%    or die "Unknown (cust_svc) svcnum!";
+%
+%  $pkgnum=$cust_svc->pkgnum;
+%  $svcpart=$cust_svc->svcpart;
+%  
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %}
 %my $action = $svc_forward->svcnum ? 'Edit' : 'Add';
 %
 %}
 %my $action = $svc_forward->svcnum ? 'Edit' : 'Add';
index 30d98f0..4b27752 100644 (file)
@@ -1,10 +1,9 @@
-<!-- mason kludge -->
-%
-%
 %my $conf = new FS::Conf;
 %
 %my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_www );
 %my $conf = new FS::Conf;
 %
 %my( $svcnum,  $pkgnum, $svcpart, $part_svc, $svc_www );
+%
 %if ( $cgi->param('error') ) {
 %if ( $cgi->param('error') ) {
+%
 %  $svc_www = new FS::svc_www ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_www')
 %  } );
 %  $svc_www = new FS::svc_www ( {
 %    map { $_, scalar($cgi->param($_)) } fields('svc_www')
 %  } );
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
 %  $svcpart = $cgi->param('svcpart');
 %  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
 %  die "No part_svc entry!" unless $part_svc;
-%} else {
-%  my($query) = $cgi->keywords;
-%  if ( $query =~ /^(\d+)$/ ) { #editing
-%    $svcnum=$1;
-%    $svc_www=qsearchs('svc_www',{'svcnum'=>$svcnum})
-%      or die "Unknown (svc_www) svcnum!";
 %
 %
-%    my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
-%      or die "Unknown (cust_svc) svcnum!";
+%} elsif ( $cgi->param('pkgnum') && $cgi->param('svcpart') ) { #adding
 %
 %
-%    $pkgnum=$cust_svc->pkgnum;
-%    $svcpart=$cust_svc->svcpart;
-%  
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%  $pkgnum = $1;
+%  $cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%  $svcpart = $1;
+%
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%  } else { #adding
+%  $svc_www = new FS::svc_www { svcpart => $svcpart };
 %
 %
-%    foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
-%      $pkgnum=$1 if /^pkgnum(\d+)$/;
-%      $svcpart=$1 if /^svcpart(\d+)$/;
-%    }
-%    $svc_www = new FS::svc_www { svcpart => $svcpart };
+%  $svcnum='';
 %
 %
-%    $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
-%    die "No part_svc entry!" unless $part_svc;
+%  $svc_www->set_default_and_fixed;
 %
 %
-%    $svcnum='';
+%} else { #editing
 %
 %
-%    $svc_www->set_default_and_fixed;
+%  my($query) = $cgi->keywords;
+%  $query =~ /^(\d+)$/ or die "unparsable svcnum";
+%  $svcnum=$1;
+%  $svc_www=qsearchs('svc_www',{'svcnum'=>$svcnum})
+%    or die "Unknown (svc_www) svcnum!";
+%
+%  my($cust_svc)=qsearchs('cust_svc',{'svcnum'=>$svcnum})
+%    or die "Unknown (cust_svc) svcnum!";
+%
+%  $pkgnum=$cust_svc->pkgnum;
+%  $svcpart=$cust_svc->svcpart;
+%  
+%  $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
+%  die "No part_svc entry!" unless $part_svc;
 %
 %
-%  }
 %}
 %my $action = $svc_www->svcnum ? 'Edit' : 'Add';
 %
 %}
 %my $action = $svc_www->svcnum ? 'Edit' : 'Add';
 %
index c543ac0..0e69b19 100644 (file)
@@ -132,7 +132,7 @@ input.fsblackbuttonselected {
         <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
           <FORM ACTION="<%$fsurl%>search/cust_main.cgi" METHOD="GET" STYLE="margin:0">
             <INPUT NAME="search_cust" TYPE="text" VALUE="(cust #, name, company or phone)" SIZE="28" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
         <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
           <FORM ACTION="<%$fsurl%>search/cust_main.cgi" METHOD="GET" STYLE="margin:0">
             <INPUT NAME="search_cust" TYPE="text" VALUE="(cust #, name, company or phone)" SIZE="28" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
-            <A HREF="<%$fsurl%>search/cust_main.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
+            <A NOTYET="<%$fsurl%>search/cust_main.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
             <INPUT TYPE="submit" VALUE="Search customers" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
           </FORM>
         </TD>
             <INPUT TYPE="submit" VALUE="Search customers" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
           </FORM>
         </TD>
@@ -155,10 +155,10 @@ input.fsblackbuttonselected {
         </TD>
 
         <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
         </TD>
 
         <TD COLSPAN=1 BGCOLOR="#000000" ALIGN="right">
-          <FORM ACTION="<%$fsurl%>search/svc_Smart.html" METHOD="GET" STYLE="margin:0">
+          <FORM ACTION="<%$fsurl%>search/cust_svc.html" METHOD="GET" STYLE="margin:0">
             <INPUT NAME="search_svc" TYPE="text" VALUE="(user, user@domain or domain)" SIZE="26" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
             <INPUT NAME="search_svc" TYPE="text" VALUE="(user, user@domain or domain)" SIZE="26" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" STYLE="vertical-align:bottom;text-align:right"><BR>
-            <A HREF="<%$fsurl%>search/svc_Smarter.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
-            <INPUT TYPE="submit" VALUE="Search services"CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
+            <A NOTYET="<%$fsurl%>search/svc_Smarter.html" STYLE="color: #000000; font-size: 70%">Advanced</A>
+            <INPUT TYPE="submit" VALUE="Search services" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
           </FORM>
         </TD>
 
           </FORM>
         </TD>
 
index 328e24a..18a499b 100644 (file)
@@ -80,58 +80,49 @@ tie my %report_invoices, 'Tie::IxHash',
   'Advanced invoice reports' => [ $fsurl.'search/report_cust_bill.html', 'by agent, date range, etc.' ],
 ;
 
   'Advanced invoice reports' => [ $fsurl.'search/report_cust_bill.html', 'by agent, date range, etc.' ],
 ;
 
-tie my %report_services_acct, 'Tie::IxHash',
-  'All accounts by username' => [ $fsurl.'search/svc_acct.cgi?username', '' ],
-  'All accounts by UID'      => [ $fsurl.'search/svc_acct.cgi?uid', '' ],
-;
-$report_services_acct{'Unlinked accounts'} = [ $fsurl.'search/svc_acct.cgi?UN_username', 'Pre-Freeside accounts without a customer record' ]
-  if $curuser->access_right('View/link unlinked services');
-
-tie my %report_services_domain, 'Tie::IxHash',
-  'All domains'      => [ $fsurl.'search/svc_domain.cgi?domain', '' ],
-;
-$report_services_domain{'Unlinked domains'} = [ $fsurl.'search/svc_domain.cgi?UN_domain', 'Pre-Freeside domains without a customer record' ]
-  if $curuser->access_right('View/link unlinked services');
-
-tie my %report_services_forward, 'Tie::IxHash',
-  'All mail forwards'      => [ $fsurl.'search/svc_forward.cgi?svcnum', '' ],
-;
-$report_services_forward{'Unlinked mail forwards'} = [ $fsurl.'search/svc_forward.cgi?UN_svcnum', 'Pre-Freeside mail forwards without a customer record' ]
-  if $curuser->access_right('View/link unlinked services');
-
-tie my %report_services_www, 'Tie::IxHash',
-  'All virtual hosts'     => [ $fsurl.'search/svc_www.cgi?svcnum', '' ],
-;
-$report_services_www{'Unlinked virtual hosts'} = [ $fsurl.'search/svc_www.cgi?UN_svcnum', 'Pre-Freeside virtual hosts without a customer record' ]
-  if $curuser->access_right('View/link unlinked services');
-
-tie my %report_services_broadband, 'Tie::IxHash',
-  'All broadband services' => [ $fsurl.'search/svc_broadband.cgi?svcnum', '' ],
-  #'Unlinked domain' => [ $fsurl.'search/svc_acct.cgi?UN_uid', 'Pre-Freeside broadband services without a customer record' ],
-;
-
-tie my %report_services_phone, 'Tie::IxHash',
-  'All phone numbers' => [ $fsurl.'search/svc_phone.cgi?svcnum', '' ],
-;
-
-tie my %report_services_external, 'Tie::IxHash',
-  'All external services' => [ $fsurl.'search/svc_external.cgi?id', '' ],
-;
-$report_services_external{'Unlinked external services'} = [ $fsurl.'search/svc_external.cgi?UN_id', 'Pre-Freeside external services without a customer record' ]
-  if $curuser->access_right('View/link unlinked services');
-
 tie my %report_services, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration') ) {
   $report_services{'Service definitions'} =  [ $fsurl.'browse/part_svc.cgi?orderby=active', 'Service definitions by number of active packages' ];
   $report_services{'separator'} =  '';
 }
 tie my %report_services, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration') ) {
   $report_services{'Service definitions'} =  [ $fsurl.'browse/part_svc.cgi?orderby=active', 'Service definitions by number of active packages' ];
   $report_services{'separator'} =  '';
 }
-$report_services{'Accounts'} =  [ \%report_services_acct, 'Access accounts and mailboxes' ];
-$report_services{'Domains'} =  [ \%report_services_domain, 'Domains', ];
-$report_services{'Mail forwards'} =  [ \%report_services_forward, 'Mail forwards', ];
-$report_services{'Virtual hosts'} =  [ \%report_services_www, 'Virtual hosting', ];
-$report_services{'Broadband services'} =  [ \%report_services_broadband, 'Fixed (username-less) broadband services', ];
-$report_services{'Phone numbers'} =  [ \%report_services_phone, 'Telephone numbers', ];
-$report_services{'External services'} =  [ \%report_services_external, 'External services', ];
+foreach my $svcdb ( FS::part_svc->svc_tables() ) {
+
+  my $name =        "FS::$svcdb"->table_info->{'name_plural'}
+             || PL( "FS::$svcdb"->table_info->{'name'}        );
+  my $lcname = lc($name);
+  my $longname = "FS::$svcdb"->table_info->{'longname_plural'} || $name;
+  my $lclongname = lc($longname);
+  my $sorts = "FS::$svcdb"->table_info->{'sorts'} || [ 'svcnum' ];
+  $sorts = [ $sorts ] unless ref($sorts);
+  my %svc_url = ( 'm'      => $m,
+                 'action' => 'search',
+                 'svcdb'  => $svcdb,
+               );
+
+  tie my %report_svc, 'Tie::IxHash';
+
+  foreach my $sort ( @$sorts ) {
+
+    my $title = "All $lcname";
+    $title .= " by ". FS::part_svc->svc_table_fields($svcdb)->{$sort}->{'label'}
+      if scalar(@$sorts) > 1;
+
+    $report_svc{$title} =
+      [ FS::UI::Web::svc_url( %svc_url, 'query' => "magic=all;sortby=$sort" ),
+        '',
+      ];
+  }
+
+  if ( $curuser->access_right('View/link unlinked services') ) {
+    $report_svc{"Unlinked $lcname"} = 
+      [ FS::UI::Web::svc_url( %svc_url, 'query' => "magic=unlinked;sortby=". $sorts->[0] ),
+        "Pre-Freeside $lcname without a customer record",
+      ];
+  }
+
+  $report_services{$name} = [ \%report_svc, $longname ];
+
+}
 
 tie my %report_packages, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration') ) {
 
 tie my %report_packages, 'Tie::IxHash';
 if ( $curuser->access_right('Configuration') ) {
index 1d1f5e1..ef72b4a 100755 (executable)
@@ -1,6 +1,3 @@
-<!-- mason kludge -->
-%
-%
 %my %link_field = (
 %  'svc_acct'    => 'username',
 %  'svc_domain'  => 'domain',
 %my %link_field = (
 %  'svc_acct'    => 'username',
 %  'svc_domain'  => 'domain',
 %                   },
 %);
 %
 %                   },
 %);
 %
-%my($query) = $cgi->keywords;
-%my($pkgnum, $svcpart) = ('', '');
-%foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
-%  $pkgnum=$1 if /^pkgnum(\d+)$/;
-%  $svcpart=$1 if /^svcpart(\d+)$/;
-%}
+%$cgi->param('pkgnum') =~ /^(\d+)$/ or die 'unparsable pkgnum';
+%my $pkgnum = $1;
+%$cgi->param('svcpart') =~ /^(\d+)$/ or die 'unparsable svcpart';
+%my $svcpart = $1;
 %
 %my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
 %my $svc = $part_svc->getfield('svc');
 %
 %my $part_svc = qsearchs('part_svc',{'svcpart'=>$svcpart});
 %my $svc = $part_svc->getfield('svc');
@@ -29,8 +24,6 @@
 %my $link_field = $link_field{$svcdb};
 %my $link_field2 = $link_field2{$svcdb};
 %
 %my $link_field = $link_field{$svcdb};
 %my $link_field2 = $link_field2{$svcdb};
 %
-%
-
 
 <% include("/elements/header.html","Link to existing $svc") %>
 <FORM ACTION="<% popurl(1) %>process/link.cgi" METHOD=POST>
 
 <% include("/elements/header.html","Link to existing $svc") %>
 <FORM ACTION="<% popurl(1) %>process/link.cgi" METHOD=POST>
index 1b6b526..e15447a 100755 (executable)
@@ -1,5 +1,3 @@
-%
-%
 %my $conf = new FS::Conf;
 %my $maxrecords = $conf->config('maxsearchrecordsperpage');
 %
 %my $conf = new FS::Conf;
 %my $maxrecords = $conf->config('maxsearchrecordsperpage');
 %
 %         my($label, $value, $svcdb) = $cust_svc->label;
 %         my($svcnum) = $cust_svc->svcnum;
 %         my($sview) = $p.'view';
 %         my($label, $value, $svcdb) = $cust_svc->label;
 %         my($svcnum) = $cust_svc->svcnum;
 %         my($sview) = $p.'view';
-%         print $n2,qq!<TD CLASS="grid" BGCOLOR="$bgcolor" ><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$label</FONT></A></TD>!,
-%               qq!<TD CLASS="grid" BGCOLOR="$bgcolor" ><A HREF="$sview/$svcdb.cgi?$svcnum"><FONT SIZE=-1>$value</FONT></A></TD>!;
+%         print $n2,
+%           qq!<TD CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !.
+%           qq!<TD CLASS="grid" BGCOLOR="$bgcolor" >!. FS::UI::Web::svc_label_link($m, $cust_svc->part_svc, $cust_svc) . qq!</TD> !;
 %         $n2="</TR><TR>";
 %      }
 %
 %         $n2="</TR><TR>";
 %      }
 %
 %
 %  \@cust_main;
 %}
 %
 %  \@cust_main;
 %}
-%
-%
-
diff --git a/httemplate/search/cust_svc.html b/httemplate/search/cust_svc.html
new file mode 100644 (file)
index 0000000..568b43b
--- /dev/null
@@ -0,0 +1,110 @@
+<% include( 'elements/search.html',
+              'title'       => 'Service search results',
+             'name'        => 'services',
+             'query'       => $sql_query,
+             'count_query' => $count_query,
+             'redirect'    => $link,
+             'header'      => [ '#',
+                                'Service',
+                                # package?
+                                FS::UI::Web::cust_header(),
+                              ],
+             'fields'      => [ 'svcnum',
+                                sub { 
+                                  #$_[0]->svc. ': '. $_[0]->label;
+                                  my($label, $value, $svcdb) = $_[0]->label;
+                                  "$label: $value";
+                                },
+                                # package?
+                                \&FS::UI::Web::cust_fields,
+                              ],
+             'links'       => [ $link,
+                                $link,
+                                # package?
+                                ( map { $link_cust }
+                                       FS::UI::Web::cust_header()
+                                 ),
+                              ],
+          )
+%>
+<%init>
+
+my $addl_from = ' LEFT JOIN part_svc  USING ( svcpart ) '.
+                ' LEFT JOIN cust_pkg  USING ( pkgnum  ) '.
+                ' LEFT JOIN cust_main USING ( custnum ) ';
+
+my @extra_sql = ();
+my $orderby = 'ORDER BY svcnum'; #has to be ordered by something
+                                 #for pagination to work
+if ( length( $cgi->param('search_svc') ) ) {
+
+  my $string = $cgi->param('search_svc');
+
+  # implement fuzzy searching in subclasses too at some point?
+  # service searching maybe shouldn't be fuzzy...
+
+  push @extra_sql,
+    ' ( '. join(' OR ',
+      map { my $table = $_;
+            my $search_sql = "FS::$table"->search_sql($string);
+            " ( svcdb = '$table'
+               AND 0 < ( SELECT COUNT(*) FROM $table
+                           WHERE $table.svcnum = cust_svc.svcnum
+                             AND $search_sql
+                       )
+             ) ";
+          }
+      FS::part_svc->svc_tables
+    ). ' ) ';
+
+} elsif ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+
+  $cgi->param('svcdb') =~ /^(svc_\w+)$/ or die "unknown svcdb";
+  push @extra_sql, "svcdb = '$1'";
+
+  push @extra_sql, 'pkgnum IS NULL'
+    if $cgi->param('magic') eq 'unlinked';
+
+  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+    my $sortby = $1;
+    $orderby = "ORDER BY $sortby";
+  }
+
+} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+
+  push @extra_sql, "svcpart = $1";
+
+} else {
+  eidiot("No search term specified");
+}
+
+#here is the agent virtualization
+push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql;
+
+my $extra_sql = ' WHERE '. join(' AND ', @extra_sql );
+
+my $sql_query = {
+  'select'     => join(', ',
+                    'cust_svc.*',
+                   'part_svc.*',
+                   'cust_main.custnum',
+                   FS::UI::Web::cust_sql_fields(),
+                  ),
+  'table'      => 'cust_svc',
+  'addl_from'  => $addl_from,
+  'hashref'    => {},
+  'extra_sql'  => "$extra_sql $orderby",
+};
+
+my $count_query = "SELECT COUNT(*) FROM cust_svc $addl_from $extra_sql";
+
+my $link = sub {
+  my $cust_svc = shift;
+  my $url = FS::UI::Web::svc_url(
+    'm'        => $m,
+    'action'   => 'view',
+    #'part_svc' => $cust_svc->part_svc,
+    'svcdb'    => $cust_svc->svcdb, #we have it from the joined search
+    #'svc'      => $cust_svc, #redundant
+    'query'     => 'svcnum=',
+  );
index 2a1414b..592aa15 100755 (executable)
@@ -1,15 +1,4 @@
-%
-%
-%my $orderby = 'ORDER BY svcnum';
-%
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%
 %my @extra_sql = ();
 %my @extra_sql = ();
-%if ( $query =~ /^UN_(.*)$/ ) {
-%  $query = $1;
-%  push @extra_sql, 'pkgnum IS NULL';
-%}
 %
 % if ( $cgi->param('domain') ) { 
 %   my $svc_domain =
 %
 % if ( $cgi->param('domain') ) { 
 %   my $svc_domain =
 %   }
 % }
 %
 %   }
 % }
 %
-%if ( $query eq 'svcnum' ) {
-%  #$orderby = "ORDER BY svcnum";
-%} elsif ( $query eq 'username' ) {
-%  $orderby = "ORDER BY LOWER(username)";
-%} elsif ( $query eq 'uid' ) {
-%  $orderby = "ORDER BY uid";
-%  push @extra_sql, "uid IS NOT NULL";
+%my $orderby = 'ORDER BY svcnum';
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
+%  push @extra_sql, 'pkgnum IS NULL'
+%    if $cgi->param('magic') eq 'unlinked';
+%
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    $sortby = "LOWER($sortby)"
+%      if $sortby eq 'username';
+%    push @extra_sql, "$sortby IS NOT NULL"
+%      if $sortby eq 'uid';
+%    $orderby = "ORDER BY $sortby";
+%  }
+%
 %} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
 %  push @extra_sql, "popnum = $1";
 %  $orderby = "ORDER BY LOWER(username)";
 %} elsif ( $cgi->param('popnum') =~ /^(\d+)$/ ) {
 %  push @extra_sql, "popnum = $1";
 %  $orderby = "ORDER BY LOWER(username)";
index ae32ccd..297d74c 100755 (executable)
@@ -1,19 +1,38 @@
-%
-%
 %my $conf = new FS::Conf;
 %
 %my $conf = new FS::Conf;
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%my(@svc_broadband,$sortby);
-%if ( $query eq 'svcnum' ) {
-%  $sortby=\*svcnum_sort;
-%  @svc_broadband=qsearch('svc_broadband',{});
-%} elsif ( $query eq 'blocknum' ) {
-%  $sortby=\*blocknum_sort;
+%my @svc_broadband = ();
+%my $sortby=\*svcnum_sort;
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
 %  @svc_broadband=qsearch('svc_broadband',{});
 %  @svc_broadband=qsearch('svc_broadband',{});
-%} else {
-%  $cgi->param('ip_addr') =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/; 
-%  my($ip_addr)=$1;
+%
+%  if ( $cgi->param('magic') eq 'unlinked' ) {
+%    @svc_broadband = grep { qsearchs('cust_svc', {
+%                                                   'svcnum' => $_->svcnum,
+%                                                   'pkgnum' => '',
+%                                                 }
+%                                    )
+%                          }
+%                    @svc_broadband;
+%  }
+%
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    if ( $sortby eq 'blocknum' ) {
+%      $sortby = \*blocknum_sort;
+%    }
+%  }
+%
+%} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%
+%  @svc_broadband =
+%    qsearch( 'svc_broadband', {}, '',
+%               " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
+%               "              WHERE cust_svc.svcnum = svc_external.svcnum ) "
+%    );
+%
+%} elsif ( $cgi->param('ip_addr') =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
+%  my $ip_addr = $1;
 %  @svc_broadband = qsearchs('svc_broadband',{'ip_addr'=>$ip_addr});
 %}
 %
 %  @svc_broadband = qsearchs('svc_broadband',{'ip_addr'=>$ip_addr});
 %}
 %
index 85ae94a..8643ea0 100755 (executable)
@@ -1,27 +1,19 @@
-%
-%
 %my $conf = new FS::Conf;
 %
 %my $conf = new FS::Conf;
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%
 %my $orderby = 'ORDER BY svcnum';
 %my %svc_domain = ();
 %my @extra_sql = ();
 %my $orderby = 'ORDER BY svcnum';
 %my %svc_domain = ();
 %my @extra_sql = ();
-%if ( $query eq 'svcnum' ) {
-%  #$orderby = 'ORDER BY svcnum';
-%} elsif ( $query eq 'domain' ) {
-%  $orderby = 'ORDER BY domain';
-%} elsif ( $query eq 'UN_svcnum' ) { #UN searches need to be acl'ed (and need to
-%                                    #fix $agentnums_sql
-%  #$orderby = 'ORDER BY svcnum';
-%  push @extra_sql, 'pkgnum IS NULL';
-%} elsif ( $query eq 'UN_domain' ) { #UN searches need to be acl'ed (and need to
-%                                    #fix $agentnums_sql
-%  $orderby = 'ORDER BY domain';
-%  push @extra_sql, 'pkgnum IS NULL';
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
+%  push @extra_sql, 'pkgnum IS NULL'
+%    if $cgi->param('magic') eq 'unlinked';
+%
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    $orderby = "ORDER BY $sortby";
+%  }
+%
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
-%  #$orderby = 'ORDER BY svcnum';
 %  push @extra_sql, "svcpart = $1";
 %} else {
 %  $cgi->param('domain') =~ /^([\w\-\.]+)$/; 
 %  push @extra_sql, "svcpart = $1";
 %} else {
 %  $cgi->param('domain') =~ /^([\w\-\.]+)$/; 
index e85d6d7..5502bfc 100755 (executable)
@@ -1,39 +1,45 @@
-%
-%
 %my $conf = new FS::Conf;
 %
 %my $conf = new FS::Conf;
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%my(@svc_external,$sortby);
-%if ( $query eq 'svcnum' ) {
-%  $sortby=\*svcnum_sort;
-%  @svc_external=qsearch('svc_external',{});
-%} elsif ( $query eq 'id' ) {
-%  $sortby=\*id_sort;
+%my @svc_external = ();
+%my @h_svc_external = ();
+%my $sortby=\*svcnum_sort;
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
 %  @svc_external=qsearch('svc_external',{});
 %  @svc_external=qsearch('svc_external',{});
-%} elsif ( $query eq 'UN_svcnum' ) {
-%  $sortby=\*svcnum_sort;
-%  @svc_external = grep qsearchs('cust_svc',{
-%      'svcnum' => $_->svcnum,
-%      'pkgnum' => '',
-%    }), qsearch('svc_external',{});
-%} elsif ( $query eq 'UN_id' ) {
-%  $sortby=\*id_sort;
-%  @svc_external = grep qsearchs('cust_svc',{
-%      'svcnum' => $_->svcnum,
-%      'pkgnum' => '',
-%    }), qsearch('svc_external',{});
+%
+%  if ( $cgi->param('magic') eq 'unlinked' ) {
+%    @svc_external = grep { qsearchs('cust_svc', {
+%                                                  'svcnum' => $_->svcnum,
+%                                                  'pkgnum' => '',
+%                                                }
+%                                   )
+%                         }
+%                   @svc_external;
+%  }
+%
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    if ( $sortby eq 'id' ) {
+%      $sortby = \*id_sort;
+%    }
+%  }
+%
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%
 %  @svc_external =
 %    qsearch( 'svc_external', {}, '',
 %               " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
 %               "              WHERE cust_svc.svcnum = svc_external.svcnum ) "
 %    );
 %  @svc_external =
 %    qsearch( 'svc_external', {}, '',
 %               " WHERE $1 = ( SELECT svcpart FROM cust_svc ".
 %               "              WHERE cust_svc.svcnum = svc_external.svcnum ) "
 %    );
-%  $sortby=\*svcnum_sort;
-%} else {
-%  $cgi->param('id') =~ /^([\w\-\.]+)$/; 
-%  my($id)=$1;
-%  #push @svc_domain, qsearchs('svc_domain',{'domain'=>$domain});
+%
+%} elsif ( $cgi->param('title') =~ /^(.*)$/ ) {
+%  $sortby=\*id_sort;
+%  @svc_external=qsearch('svc_external',{ title => $1 });
+%  if( $cgi->param('history') == 1 ) {
+%    @h_svc_external=qsearch('h_svc_external',{ title => $1 });
+%  }
+%} elsif ( $cgi->param('id') =~ /^([\w\-\.]+)$/ ) {
+%  my $id = $1;
 %  @svc_external = qsearchs('svc_external',{'id'=>$id});
 %}
 %
 %  @svc_external = qsearchs('svc_external',{'id'=>$id});
 %}
 %
 %    print "</TR>";
 %
 %  }
 %    print "</TR>";
 %
 %  }
-% 
+%  if( scalar(@h_svc_external) > 0 ) {
+%    print <<HTML;
+%    </TABLE>
+%    <TABLE BORDER=4 CELLSPACING=0 CELLPADDING=0>
+%      <TR>
+%        <TH>Freeside ID</TH>
+%        <TH>Service #</TH>
+%        <TH>Title</TH>
+%        <TH>Date</TH>
+%      </TR>
+%HTML
+%
+%    foreach my $h_svc ( @h_svc_external ) {
+%        my($svcnum, $id, $title, $user, $date)=(
+%            $h_svc->svcnum,
+%            $h_svc->id,
+%            $h_svc->title,
+%            $h_svc->history_user,
+%            $h_svc->history_date,
+%        );
+%        my $rowspan = 1;
+%        my ($h_cust_svc) = qsearchs( 'h_cust_svc', {
+%            svcnum  =>  $svcnum,
+%        });
+%        my $cust_pkg = qsearchs( 'cust_pkg', {
+%            pkgnum  =>  $h_cust_svc->pkgnum,
+%        });
+%        my $custnum = $cust_pkg->custnum;
+%
+%        print <<END;
+%        <TR>
+%          <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$custnum</A></TD>
+%          <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$svcnum</A></TD>
+%          <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$title</A></TD>
+%          <TD ROWSPAN=$rowspan><A HREF="${p}view/cust_main.cgi?$custnum">$date</A></TD>
+%        </TR>
+%END
+%    }
+%  }
+%
 %  print <<END;
 %    </TABLE>
 %  </BODY>
 %  print <<END;
 %    </TABLE>
 %  </BODY>
index dc002d9..4d44c9c 100755 (executable)
@@ -1,23 +1,19 @@
-%
-%
 %my $conf = new FS::Conf;
 %
 %my $conf = new FS::Conf;
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
+%my $orderby = 'ORDER BY svcnum';
+%my @extra_sql = ();
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 %
 %
-%my $orderby;
+%  push @extra_sql, 'pkgnum IS NULL'
+%    if $cgi->param('magic') eq 'unlinked';
 %
 %
-%my @extra_sql = ();
-%if ( $query =~ /^UN_(.*)$/ ) { #UN searches need to be acl'ed (and need to
-%                                    #fix $agentnums_sql
-%  $query = $1;
-%  push @extra_sql, 'pkgnum IS NULL';
-%}
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    $orderby = "ORDER BY $sortby";
+%  }
 %
 %
-%if ( $query eq 'svcnum' ) {
-%  $orderby = 'ORDER BY svcnum';
-%} else {
-%  eidiot('unimplemented');
+%} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%  push @extra_sql, "svcpart = $1";
 %}
 %
 %my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
 %}
 %
 %my $addl_from = ' LEFT JOIN cust_svc  USING ( svcnum  ) '.
index 26e2090..229dd5d 100644 (file)
@@ -1,19 +1,19 @@
-%
-%
 %my $conf = new FS::Conf;
 %
 %my $conf = new FS::Conf;
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%
 %my $orderby = 'ORDER BY svcnum';
 %my %svc_phone = ();
 %my @extra_sql = ();
 %my $orderby = 'ORDER BY svcnum';
 %my %svc_phone = ();
 %my @extra_sql = ();
-%if ( $query eq 'svcnum' ) {
-%  #$orderby = 'ORDER BY svcnum';
-%} elsif ( $query eq 'phonenum' ) {
-%  $orderby = 'ORDER BY phonenum';
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
+%
+%  push @extra_sql, 'pkgnum IS NULL'
+%    if $cgi->param('magic') eq 'unlinked';
+%
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    $orderby = "ORDER BY $sortby";
+%  }
+%
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
 %} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
-%  #$orderby = 'ORDER BY svcnum';
 %  push @extra_sql, "svcpart = $1";
 %} else {
 %  $cgi->param('phonenum') =~ /^([\d\- ]+)$/; 
 %  push @extra_sql, "svcpart = $1";
 %} else {
 %  $cgi->param('phonenum') =~ /^([\d\- ]+)$/; 
index b0f1d5c..ae1482b 100755 (executable)
@@ -1,16 +1,35 @@
+%#my $conf = new FS::Conf;
 %
 %
+%my $orderby = 'ORDER BY svcnum';
+%my @extra_sql = ();
+%if ( $cgi->param('magic') =~ /^(all|unlinked)$/ ) {
 %
 %
-%#my $conf = new FS::Conf;
+%  push @extra_sql, 'pkgnum IS NULL'
+%    if $cgi->param('magic') eq 'unlinked';
 %
 %
-%my($query)=$cgi->keywords;
-%$query ||= ''; #to avoid use of unitialized value errors
-%my $orderby;
-%if ( $query eq 'svcnum' ) {
-%  $orderby = 'ORDER BY svcnum';
-%} else {
-%  eidiot('unimplemented');
+%  if ( $cgi->param('sortby') =~ /^(\w+)$/ ) {
+%    my $sortby = $1;
+%    $orderby = "ORDER BY $sortby";
+%  }
+%
+%} elsif ( $cgi->param('svcpart') =~ /^(\d+)$/ ) {
+%  push @extra_sql, "svcpart = $1";
 %}
 %
 %}
 %
+%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;
+%
+%my $extra_sql = 
+%  scalar(@extra_sql)
+%    ? ' WHERE '. join(' AND ', @extra_sql )
+%    : '';
+%
+%
 %my $count_query = 'SELECT COUNT(*) FROM svc_www';
 %my $sql_query = {
 %  'table'     => 'svc_www',
 %my $count_query = 'SELECT COUNT(*) FROM svc_www';
 %my $sql_query = {
 %  'table'     => 'svc_www',
@@ -22,9 +41,7 @@
 %                   FS::UI::Web::cust_sql_fields(),
 %                 ),
 %  'extra_sql' => $orderby,
 %                   FS::UI::Web::cust_sql_fields(),
 %                 ),
 %  'extra_sql' => $orderby,
-%  'addl_from' => 'LEFT JOIN cust_svc  USING ( svcnum  )'.
-%                 'LEFT JOIN cust_pkg  USING ( pkgnum  )'.
-%                 'LEFT JOIN cust_main USING ( custnum )',
+%  'addl_from' => $addl_from,
 %};
 %
 %my $link  = [ "${p}view/svc_www.cgi?", 'svcnum', ];
 %};
 %
 %my $link  = [ "${p}view/svc_www.cgi?", 'svcnum', ];
index 0a386f2..8416050 100755 (executable)
@@ -1,11 +1,9 @@
-%
 %  my( $cust_main ) = @_;
 %  my $conf = new FS::Conf;
 %
 %  my $curuser = $FS::CurrentUser::CurrentUser;
 %
 %  my $packages = get_packages($cust_main, $conf);
 %  my( $cust_main ) = @_;
 %  my $conf = new FS::Conf;
 %
 %  my $curuser = $FS::CurrentUser::CurrentUser;
 %
 %  my $packages = get_packages($cust_main, $conf);
-%
 
 
 <A NAME="cust_pkg"><FONT SIZE="+2">Packages</FONT></A>
 
 
 <A NAME="cust_pkg"><FONT SIZE="+2">Packages</FONT></A>
@@ -58,44 +56,42 @@ Current packages
 % my $bgcolor1 = '#eeeeee';
 %   my $bgcolor2 = '#ffffff';
 %   my $bgcolor = '';
 % my $bgcolor1 = '#eeeeee';
 %   my $bgcolor2 = '#ffffff';
 %   my $bgcolor = '';
-%
-
 
 <TR>
   <TH CLASS="grid" BGCOLOR="#cccccc">Package</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Services</TH>
 </TR>
 
 <TR>
   <TH CLASS="grid" BGCOLOR="#cccccc">Package</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Status</TH>
   <TH CLASS="grid" BGCOLOR="#cccccc">Services</TH>
 </TR>
+
+%foreach my $cust_pkg (@$packages) {
 %
 %
-%foreach my $pkg (sort pkgsort_pkgnum_cancel @$packages) {
+%  my $part_pkg = $cust_pkg->part_pkg;
 %
 %  if ( $bgcolor eq $bgcolor1 ) {
 %    $bgcolor = $bgcolor2;
 %  } else {
 %    $bgcolor = $bgcolor1;
 %  }
 %
 %  if ( $bgcolor eq $bgcolor1 ) {
 %    $bgcolor = $bgcolor2;
 %  } else {
 %    $bgcolor = $bgcolor1;
 %  }
-%
-%
 
 
 
 
-<!--pkgnum: <%$pkg->{pkgnum}%>-->
+<!--pkgnum: <% $cust_pkg->pkgnum %>-->
 <TR>
   <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
 <TR>
   <TD CLASS="grid" BGCOLOR="<% $bgcolor %>">
-    <A NAME="cust_pkg<%$pkg->{pkgnum}%>"><%$pkg->{pkgnum}%></A>:
-    <%$pkg->{pkg}%> - <%$pkg->{comment}%><BR>
+    <A NAME="cust_pkg<% $cust_pkg->pkgnum %>"><% $cust_pkg->pkgnum %></A>:
+    <% $part_pkg->pkg %> - <% $part_pkg->comment %><BR>
     <FONT SIZE=-1>
     <FONT SIZE=-1>
-% unless ( $pkg->{cancel} ) { 
+% unless ( $cust_pkg->get('cancel') ) { 
 % if ( $curuser->access_right('Change customer package') ) { 
 
 % if ( $curuser->access_right('Change customer package') ) { 
 
-            (&nbsp;<%pkg_change_link($pkg)%>&nbsp;)
+            (&nbsp;<%pkg_change_link($cust_pkg)%>&nbsp;)
 % } 
 % if ( $curuser->access_right('Edit customer package dates') ) { 
 
 % } 
 % if ( $curuser->access_right('Edit customer package dates') ) { 
 
-            (&nbsp;<%pkg_dates_link($pkg)%>&nbsp;)
+            (&nbsp;<%pkg_dates_link($cust_pkg)%>&nbsp;)
 % } 
 % if ( $curuser->access_right('Customize customer package') ) { 
 
 % } 
 % if ( $curuser->access_right('Customize customer package') ) { 
 
-            (&nbsp;<%pkg_customize_link($pkg,$cust_main->custnum)%>&nbsp;)
+            (&nbsp;<%pkg_customize_link($cust_pkg,$cust_main->custnum)%>&nbsp;)
 % } 
 % } 
 
 % } 
 % } 
 
@@ -118,7 +114,7 @@ Current packages
 %
 %  #false laziness w/edit/REAL_cust_pkg.cgi
 %  my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
 %
 %  #false laziness w/edit/REAL_cust_pkg.cgi
 %  my( $billed_or_prepaid, $last_bill_or_renewed, $next_bill_or_prepaid_until );
-%  unless ( $pkg->{'part_pkg'}->is_prepaid ) {
+%  unless ( $part_pkg->is_prepaid ) {
 %    $billed_or_prepaid = 'billed';
 %    $last_bill_or_renewed = 'Last&nbsp;bill';
 %    $next_bill_or_prepaid_until = 'Next&nbsp;bill';
 %    $billed_or_prepaid = 'billed';
 %    $last_bill_or_renewed = 'Last&nbsp;bill';
 %    $next_bill_or_prepaid_until = 'Next&nbsp;bill';
@@ -129,19 +125,19 @@ Current packages
 %  }
 %
 %
 %  }
 %
 %
-% if ( $pkg->{cancel} ) { 
+% if ( $cust_pkg->get('cancel') ) { 
  <!-- #status: cancelled -->
 
   <TR>
     <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>
  <!-- #status: cancelled -->
 
   <TR>
     <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000"><B>Cancelled&nbsp;</B></FONT></TD>
-    <% pkg_datestr($pkg,'cancel',$conf) %>
+    <% pkg_datestr($cust_pkg, 'cancel', $conf) %>
   </TR>
   <TR>
     <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000" SIZE="-2">
   </TR>
   <TR>
     <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#ff0000" SIZE="-2">
-      <% $pkg->{reason} %>
+      <% $cust_pkg->last_reason ? $cust_pkg->last_reason->reason : '' %>
     </FONT></TD>
   </TR>
     </FONT></TD>
   </TR>
-% unless ( $pkg->{setup} ) { 
+% unless ( $cust_pkg->get('setup') ) { 
 
 
     <TR>
 
 
     <TR>
@@ -152,37 +148,37 @@ Current packages
 
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
 
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
-      <% pkg_datestr($pkg, 'setup',$conf) %>
+      <% pkg_datestr($cust_pkg, 'setup', $conf) %>
     </TR>
     </TR>
-% if ( $pkg->{'last_bill'} ) { 
+% if ( $cust_pkg->get('last_bill') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
-        <% pkg_datestr($pkg, 'last_bill',$conf) %>
+        <% pkg_datestr($cust_pkg, 'last_bill',$conf) %>
       </TR>
 % } 
       </TR>
 % } 
-% if ( $pkg->{'susp'} ) { 
+% if ( $cust_pkg->get('susp') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Suspended&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Suspended&nbsp;</TD>
-        <% pkg_datestr($pkg, 'susp',$conf) %>
+        <% pkg_datestr($cust_pkg, 'susp', $conf) %>
       </TR>
 % } 
 % } 
 % } else { 
       </TR>
 % } 
 % } 
 % } else { 
-% if ( $pkg->{susp} ) { 
+% if ( $cust_pkg->get('susp') ) { 
  <!-- #status: suspended -->
 
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>
  <!-- #status: suspended -->
 
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900"><B>Suspended</B>&nbsp;</FONT></TD>
-      <% pkg_datestr($pkg,'susp',$conf) %>
+      <% pkg_datestr($cust_pkg, 'susp', $conf) %>
     </TR>
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900" SIZE="-2">
     </TR>
     <TR>
       <TD WIDTH="<%$width%>" ALIGN="right"><FONT COLOR="#FF9900" SIZE="-2">
-        <% $pkg->{reason} %>
+        <% $cust_pkg->last_reason ? $cust_pkg->last_reason->reason : '' %>
       </FONT></TD>
     </TR>
       </FONT></TD>
     </TR>
-% unless ( $pkg->{setup} ) { 
+% unless ( $cust_pkg->get('setup') ) { 
 
 
       <TR>
 
 
       <TR>
@@ -193,24 +189,24 @@ Current packages
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
-        <% pkg_datestr($pkg, 'setup',$conf) %>
+        <% pkg_datestr($cust_pkg, 'setup', $conf) %>
       </TR>
 % } 
       </TR>
 % } 
-% if ( $pkg->{'last_bill'} ) { 
+% if ( $cust_pkg->get('last_bill') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
-        <% pkg_datestr($pkg, 'last_bill',$conf) %>
+        <% pkg_datestr($cust_pkg, 'last_bill', $conf) %>
       </TR>
 % } 
 
 
     <!-- # next bill ?? -->
       </TR>
 % } 
 
 
     <!-- # next bill ?? -->
-% if ( $pkg->{'expire'} ) { 
+% if ( $cust_pkg->get('expire') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
-        <% pkg_datestr($pkg, 'expire',$conf) %>
+        <% pkg_datestr($cust_pkg, 'expire', $conf) %>
       </TR>
 % } 
 
       </TR>
 % } 
 
@@ -220,11 +216,11 @@ Current packages
         <FONT SIZE=-1>
 % if ( $curuser->access_right('Unsuspend customer package') ) { 
 
         <FONT SIZE=-1>
 % if ( $curuser->access_right('Unsuspend customer package') ) { 
 
-            (&nbsp;<% pkg_unsuspend_link($pkg) %>&nbsp;)
+            (&nbsp;<% pkg_unsuspend_link($cust_pkg) %>&nbsp;)
 % } 
 % if ( $curuser->access_right('Cancel customer package') ) { 
 
 % } 
 % if ( $curuser->access_right('Cancel customer package') ) { 
 
-            (&nbsp;<% pkg_cancel_link($pkg) %>&nbsp;)
+            (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
 % } 
 
         </FONT>
 % } 
 
         </FONT>
@@ -232,9 +228,9 @@ Current packages
     </TR>
 % } else { 
  <!-- #status: active -->
     </TR>
 % } else { 
  <!-- #status: active -->
-% unless ( $pkg->{setup} ) { 
+% unless ( $cust_pkg->get('setup') ) { 
  <!-- #not setup -->
  <!-- #not setup -->
-% unless ( $pkg->{'freq'} ) { 
+% unless ( $part_pkg->freq ) { 
 
 
         <TR>
 
 
         <TR>
@@ -246,7 +242,7 @@ Current packages
             <FONT SIZE=-1>
 % if ( $curuser->access_right('Cancel customer package immediately') ) { 
 
             <FONT SIZE=-1>
 % if ( $curuser->access_right('Cancel customer package immediately') ) { 
 
-                (&nbsp;<% pkg_cancel_link($pkg) %>&nbsp;)
+                (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
 % } 
 
             </FONT>
 % } 
 
             </FONT>
@@ -256,12 +252,12 @@ Current packages
 
 
         <TR>
 
 
         <TR>
-          <TD COLSPAN=<%$colspan%>>Not&nbsp;yet&nbsp;billed&nbsp;(<% $billed_or_prepaid %>&nbsp;<% myfreq($pkg->{part_pkg}) %>)</TD>
+          <TD COLSPAN=<%$colspan%>>Not&nbsp;yet&nbsp;billed&nbsp;(<% $billed_or_prepaid %>&nbsp;<% myfreq($part_pkg) %>)</TD>
         </TR>
 % } 
 % } else { 
  <!-- #setup -->
         </TR>
 % } 
 % } else { 
  <!-- #setup -->
-% unless ( $pkg->{freq} ) { 
+% unless ( $part_pkg->freq ) { 
 
 
         <TR>
 
 
         <TR>
@@ -270,58 +266,58 @@ Current packages
 
         <TR>
           <TD WIDTH="<%$width%>" ALIGN="right">Billed&nbsp;</TD>
 
         <TR>
           <TD WIDTH="<%$width%>" ALIGN="right">Billed&nbsp;</TD>
-          <% pkg_datestr($pkg,'setup',$conf) %>
+          <% pkg_datestr($cust_pkg, 'setup', $conf) %>
         </TR>
 % } else { 
 
 
         <TR>
         </TR>
 % } else { 
 
 
         <TR>
-          <TD COLSPAN=<%$colspan%>><FONT COLOR="#00CC00"><B>Active</B></FONT>,&nbsp;<% $billed_or_prepaid %>&nbsp;<% myfreq($pkg->{part_pkg}) %></TD>
+          <TD COLSPAN=<%$colspan%>><FONT COLOR="#00CC00"><B>Active</B></FONT>,&nbsp;<% $billed_or_prepaid %>&nbsp;<% myfreq($part_pkg) %></TD>
         </TR>
 
         <TR>
           <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
         </TR>
 
         <TR>
           <TD WIDTH="<%$width%>" ALIGN="right">Setup&nbsp;</TD>
-          <% pkg_datestr($pkg, 'setup',$conf) %>
+          <% pkg_datestr($cust_pkg, 'setup', $conf) %>
         </TR>
 % } 
 % } 
         </TR>
 % } 
 % } 
-% if ( $pkg->{'last_bill'} ) { 
+% if ( $cust_pkg->get('last_bill') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $last_bill_or_renewed %>&nbsp;</TD>
-        <% pkg_datestr($pkg, 'last_bill',$conf) %>
+        <% pkg_datestr($cust_pkg, 'last_bill', $conf) %>
       </TR>
 % } 
       </TR>
 % } 
-% if ( $pkg->{'next_bill'} ) { 
+% if ( $cust_pkg->get('bill') ) { #next bill
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $next_bill_or_prepaid_until %>&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right"><% $next_bill_or_prepaid_until %>&nbsp;</TD>
-        <% pkg_datestr($pkg, 'next_bill',$conf) %>
+        <% pkg_datestr($cust_pkg, 'bill', $conf) %>
       </TR>
 % } 
       </TR>
 % } 
-% if ( $pkg->{'expire'} ) { 
+% if ( $cust_pkg->get('expire') ) { 
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
 
       <TR>
         <TD WIDTH="<%$width%>" ALIGN="right">Expires&nbsp;</TD>
-        <% pkg_datestr($pkg, 'expire',$conf) %>
+        <% pkg_datestr($cust_pkg, 'expire', $conf) %>
       </TR>
 % } 
       </TR>
 % } 
-% if ( $pkg->{freq} ) { 
+% if ( $part_pkg->freq ) { 
 
       <TR>
         <TD COLSPAN=<%$colspan%>>
           <FONT SIZE=-1>
 % if ( $curuser->access_right('Suspend customer package') ) { 
 
 
       <TR>
         <TD COLSPAN=<%$colspan%>>
           <FONT SIZE=-1>
 % if ( $curuser->access_right('Suspend customer package') ) { 
 
-              (&nbsp;<% pkg_suspend_link($pkg) %>&nbsp;)
+              (&nbsp;<% pkg_suspend_link($cust_pkg) %>&nbsp;)
 % } 
 % if ( $curuser->access_right('Cancel customer package immediately') ) { 
 
 % } 
 % if ( $curuser->access_right('Cancel customer package immediately') ) { 
 
-              (&nbsp;<% pkg_cancel_link($pkg) %>&nbsp;)
+              (&nbsp;<% pkg_cancel_link($cust_pkg) %>&nbsp;)
 % } 
 % if ( $curuser->access_right('Cancel customer package later') ) { 
 
 % } 
 % if ( $curuser->access_right('Cancel customer package later') ) { 
 
-              (&nbsp;<% pkg_expire_link($pkg) %>&nbsp;)
+              (&nbsp;<% pkg_expire_link($cust_pkg) %>&nbsp;)
 % } 
 
           <FONT>
 % } 
 
           <FONT>
@@ -338,46 +334,49 @@ Current packages
 <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
   <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
 
 <TD CLASS="inv" BGCOLOR="<% $bgcolor %>">
   <TABLE CLASS="inv" BORDER=0 CELLSPACING=0 CELLPADDING=0 WIDTH="100%">
 
-%  foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+%  #foreach my $svcpart (sort {$a->{svcpart} <=> $b->{svcpart}} @{$pkg->{svcparts}}) {
+%  foreach my $part_svc ( $cust_pkg->part_svc ) {
 
 
-%    foreach my $service (@{$svcpart->{services}}) {
+%    #foreach my $service (@{$svcpart->{services}}) {
+%    foreach my $cust_svc ( @{ $part_svc->cust_pkg_svc } ) {
 
       <TR>
 
       <TR>
-        <TD ALIGN="right" VALIGN="top"><%svc_link($svcpart,$service)%></TD>
-        <TD STYLE="padding-bottom:0px"><B><%svc_label_link($svcpart,$service)%></B></TD>
+        <TD ALIGN="right" VALIGN="top"><% FS::UI::Web::svc_link($m, $part_svc, $cust_svc) %></TD>
+        <TD STYLE="padding-bottom:0px"><B><% FS::UI::Web::svc_label_link($m, $part_svc, $cust_svc) %></B></TD>
       </TR>
 
       </TR>
 
-        <TR>
-          <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
-
-%      if ( $curuser->access_right('Recharge customer service')
-%        && ($svcpart->{'svcdb'} eq 'svc_acct') 
-%        && ($service->{seconds} ne ''
-%         || $service->{upbytes} ne ''
-%         || $service->{downbytes} ne ''
-%         || $service->{totalbytes} ne '' )
+      <TR>
+        <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
+
+%         if ( $curuser->access_right('Recharge customer service')
+%              && $cust_svc->svcdb eq 'svc_acct'
+%              && (    $cust_svc->svc_x->seconds    ne ''
+%                   || $cust_svc->svc_x->upbytes    ne ''
+%                   || $cust_svc->svc_x->downbytes  ne ''
+%                   || $cust_svc->svc_x->totalbytes ne ''
+%                 )
 %         ) { 
 %         ) { 
-          (&nbsp;<%svc_recharge_link($service)%>&nbsp;)
-%     } 
+            (&nbsp;<%svc_recharge_link($cust_svc)%>&nbsp;)
+%         
           </FONT></TD>
 
           <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
 
           </FONT></TD>
 
           <TD ALIGN="right" VALIGN="top" STYLE="padding-bottom:5px;padding-top:0px"><FONT SIZE="-2">
 
-%      if ( $curuser->access_right('Unprovision customer service') ) { 
-          (&nbsp;<%svc_unprovision_link($service)%>&nbsp;)
-%     } 
+%         if ( $curuser->access_right('Unprovision customer service') ) { 
+            (&nbsp;<%svc_unprovision_link($cust_svc)%>&nbsp;)
+%         
           </FONT></TD>
         </TR>
 %   } 
 
           </FONT></TD>
         </TR>
 %   } 
 
-%   if (    ! $pkg->{'cancel'}
+%   if (    ! $cust_pkg->get('cancel')
 %        && $curuser->access_right('Provision customer service') 
 %        && $curuser->access_right('Provision customer service') 
-%        && $svcpart->{count} < $svcpart->{quantity}
+%        && $part_svc->num_avail
 %      ) {
 
       <TR>
         <TD COLSPAN=2 ALIGN="center" STYLE="padding-bottom:4px;padding-top:0px">
 %      ) {
 
       <TR>
         <TD COLSPAN=2 ALIGN="center" STYLE="padding-bottom:4px;padding-top:0px">
-          <B><% svc_provision_link($pkg, $svcpart, $conf, $curuser) %></B>
+          <B><% svc_provision_link($cust_pkg, $part_svc, $conf, $curuser) %></B>
         </TD>
       </TR>
 
         </TD>
       </TR>
 
@@ -414,104 +413,33 @@ Current packages
 %  } else {
 %    $method = 'all_pkgs';
 %  }
 %  } else {
 %    $method = 'all_pkgs';
 %  }
-%  
-%  foreach my $cust_pkg ( $cust_main->$method() ) {
-%  
-%    my $part_pkg = $cust_pkg->part_pkg;
-%
-%    my %pkg = ();
-%
-%    #to get back to the original object... should use it in the first place!!
-%    $pkg{cust_pkg} = $cust_pkg;
-%    $pkg{part_pkg} = $part_pkg;
-%
-%    $pkg{pkgnum} = $cust_pkg->pkgnum;
-%    $pkg{pkg} = $part_pkg->pkg;
-%    $pkg{pkgpart} = $part_pkg->pkgpart;
-%    $pkg{comment} = $part_pkg->getfield('comment');
-%    $pkg{freq} = $part_pkg->freq;
-%    $pkg{setup} = $cust_pkg->getfield('setup');
-%    $pkg{last_bill} = $cust_pkg->getfield('last_bill');
-%    $pkg{next_bill} = $cust_pkg->getfield('bill');
-%    $pkg{susp} = $cust_pkg->getfield('susp');
-%    $pkg{expire} = $cust_pkg->getfield('expire');
-%    $pkg{cancel} = $cust_pkg->getfield('cancel');
-%    $pkg{reason} = $cust_pkg->last_reason->reason if $cust_pkg->last_reason;
-%
-%  
-%    my %svcparts = map {
-%      $_->svcpart => {
-%                       $_->part_svc->hash,
-%                       'quantity' => $_->quantity,
-%                       'count'    => $cust_pkg->num_cust_svc($_->svcpart),
-%                       #'services' => [],
-%                     };
-%    } $part_pkg->pkg_svc;
-%
-%    foreach my $cust_svc ( $cust_pkg->cust_svc ) {
-%      #warn "svcnum ". $cust_svc->svcnum. " / svcpart ". $cust_svc->svcpart. "\n";
-%      my $svc = {
-%        'svcnum' => $cust_svc->svcnum,
-%        'label'  => ($cust_svc->label)[1],
-%        $cust_svc->svc_x->hash,
-%      };
-%
-%      #false laziness with above, to catch extraneous services.  whole
-%      #damn thing should be OO...
-%      my $svcpart = ( $svcparts{$cust_svc->svcpart} ||= {
-%        $cust_svc->part_svc->hash,
-%        'quantity' => 0,
-%        'count'    => $cust_pkg->num_cust_svc($cust_svc->svcpart),
-%        #'services' => [],
-%      } );
-%
-%      push @{$svcpart->{services}}, $svc;
-%
-%    }
-%
-%    $pkg{svcparts} = [ values %svcparts ];
-%
-%    push @packages, \%pkg;
-%  
-%  }
-%  
-%  return \@packages;
 %
 %
+%  [ $cust_main->$method() ];
 %}
 %}
-%
-%sub svc_link {
-%
-%  my ($svcpart, $svc) = (shift,shift) or return '';
-%  return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svcpart->{svc}</A>!;
-%
-%}
-%
-%sub svc_label_link {
-%
-%  my ($svcpart, $svc) = (shift,shift) or return '';
-%  return qq!<A HREF="${p}view/$svcpart->{svcdb}.cgi?$svc->{svcnum}">$svc->{label}</A>!;
-%
-%}
-%
+%  
 %sub svc_provision_link {
 %sub svc_provision_link {
-%  my ($pkg, $svcpart, $conf, $curuser) = @_;
-%  ( my $svc_nbsp = $svcpart->{svc} ) =~ s/\s+/&nbsp;/g;
-%  my $num_left = $svcpart->{quantity} - $svcpart->{count};
-%  my $pkgnum_svcpart = "pkgnum$pkg->{pkgnum}-svcpart$svcpart->{svcpart}";
-%
+%  my ($cust_pkg, $part_svc, $conf, $curuser) = @_;
+%  ( my $svc_nbsp = $part_svc->svc ) =~ s/\s+/&nbsp;/g;
+%  my $num_avail = $part_svc->num_avail;
+%  my $pkgnum_svcpart = "pkgnum=". $cust_pkg->pkgnum. ';'.
+%                       "svcpart=". $part_svc->svcpart;
 %  my $url;
 %  my $url;
-%  if ( $svcpart->{svcdb} eq 'svc_external'
+%  if ( $part_svc->svcdb eq 'svc_external' #could be generalized
 %       && $conf->exists('svc_external-skip_manual')
 %  ) {
 %       && $conf->exists('svc_external-skip_manual')
 %  ) {
-%    $url = "${p}edit/process/$svcpart->{svcdb}.cgi?".
-%           "pkgnum=$pkg->{pkgnum}&".
-%           "svcpart=$svcpart->{svcpart}";
+%    $url = "${p}edit/process/". $part_svc->svcdb. ".cgi?$pkgnum_svcpart";
 %  } else {
 %  } else {
-%    $url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
+%    $url = FS::UI::Web::svc_url(
+%                                 'm'        => $m,
+%                                 'action'   => 'edit',
+%                                 'part_svc' => $part_svc, 
+%                                 'query'    => $pkgnum_svcpart,
+%                               );
+%    #$url = "${p}edit/$svcpart->{svcdb}.cgi?$pkgnum_svcpart";
 %  }
 %
 %  my $link = qq!<A CLASS="provision" HREF="$url">!.
 %  }
 %
 %  my $link = qq!<A CLASS="provision" HREF="$url">!.
-%             "Provision&nbsp;$svc_nbsp&nbsp;($num_left)</A>";
+%             "Provision&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
 %  if ( $conf->exists('legacy_link')
 %       && $curuser->access_right('View/link unlinked services')
 %     )
 %  if ( $conf->exists('legacy_link')
 %       && $curuser->access_right('View/link unlinked services')
 %     )
@@ -519,39 +447,20 @@ Current packages
 %    $link .= '<BR>'.
 %             qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
 %             qq!$pkgnum_svcpart">!.
 %    $link .= '<BR>'.
 %             qq!<A CLASS="provision" HREF="${p}misc/link.cgi?!.
 %             qq!$pkgnum_svcpart">!.
-%            "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_left)</A>";
+%            "Link&nbsp;to&nbsp;legacy&nbsp;$svc_nbsp&nbsp;($num_avail)</A>";
 %  }
 %  $link;
 %}
 %
 %sub svc_unprovision_link {
 %  }
 %  $link;
 %}
 %
 %sub svc_unprovision_link {
-%  my $svc = shift or return '';
-%  qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?$svc->{svcnum}',!.
-%  qq!'Permanently unprovision and delete this service?')">Unprovision</A>!;
-%}
-%
-%sub svc_recharge_link {
-%  my $svc = shift or return '';
-%
-%  qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('${p}misc/recharge_svc.html?svcnum=$svc->{svcnum}', 392, 336, 'recharge_svc_popup' ), CAPTION, 'Recharge service $svc->{svcnum}', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Recharge</A>!;
-%}
-%
-%# This should be generalized to use config options to determine order.
-%sub pkgsort_pkgnum_cancel {
-%  if ($a->{cancel} and $b->{cancel}) {
-%    return ($a->{pkgnum} <=> $b->{pkgnum});
-%  } elsif ($a->{cancel} or $b->{cancel}) {
-%    return (-1) if ($b->{cancel});
-%    return (1) if ($a->{cancel});
-%    return (0);
-%  } else {
-%    return($a->{pkgnum} <=> $b->{pkgnum});
-%  }
+%  my $cust_svc = shift or return '';
+%  qq!<A HREF="javascript:areyousure('${p}misc/unprovision.cgi?!. $cust_svc->svcnum.
+%  qq!', 'Permanently unprovision and delete this service?')">Unprovision</A>!;
 %}
 %
 %sub pkg_datestr {
 %}
 %
 %sub pkg_datestr {
-%  my($pkg, $field, $conf) = @_ or return '';
-%  return '&nbsp;' unless $pkg->{$field};
+%  my($cust_pkg, $field, $conf) = @_ or return '';
+%  return '&nbsp;' unless $cust_pkg->get($field);
 %  my $format = '<TD align="left"><B>%b</B></TD>'.
 %               '<TD align="right"><B>&nbsp;%o,</B></TD>'.
 %               '<TD align="right"><B>&nbsp;%Y</B></TD>';
 %  my $format = '<TD align="left"><B>%b</B></TD>'.
 %               '<TD align="right"><B>&nbsp;%o,</B></TD>'.
 %               '<TD align="right"><B>&nbsp;%Y</B></TD>';
@@ -561,48 +470,68 @@ Current packages
 %             '<TD ALIGN="left"><B>%M</B></TD>'.
 %             '<TD ALIGN="left"><B>&nbsp;%P</B></TD>'
 %    if $conf->exists('cust_pkg-display_times');
 %             '<TD ALIGN="left"><B>%M</B></TD>'.
 %             '<TD ALIGN="left"><B>&nbsp;%P</B></TD>'
 %    if $conf->exists('cust_pkg-display_times');
-%  ( my $strip = time2str($format, $pkg->{$field}) ) =~ s/ (\d)/$1/g;
+%  my $strip = time2str($format, $cust_pkg->get($field) );
+%  $strip =~ s/ (\d)/$1/g;
 %  $strip;
 %}
 %
 %  $strip;
 %}
 %
-%sub pkg_change_link {
-%  my $pkg = shift or return '';
-%  return qq!<a href="${p}misc/change_pkg.cgi?$pkg->{pkgnum}">!.
-%         qq!Change&nbsp;package</a>!;
+%sub pkg_change_link    { pkg_link('misc/change_pkg',    'Change&nbsp;package', @_ ); }
+%sub pkg_suspend_link   { pkg_link('misc/susp_pkg',      'Suspend',             @_ ); }
+%sub pkg_unsuspend_link { pkg_link('misc/unsusp_pkg',    'Unsuspend',           @_ ); }
+%sub pkg_expire_link    { pkg_link('misc/expire_pkg',    'Cancel&nbsp;later',   @_ ); }
+%sub pkg_dates_link     { pkg_link('edit/REAL_cust_pkg', 'Edit&nbsp;dates',     @_ ); }
+%
+%sub pkg_cancel_link    { pkg_popup_link( 'misc/cancel_pkg.html?method=cancel',
+%                                         'Cancel&nbsp;now',
+%                                         'Cancel',
+%                                         @_
+%                                       );
+%                       }
+%sub pkg_expire_link    { pkg_popup_link( 'misc/cancel_pkg.html?method=expire',
+%                                         'Cancel&nbsp;later',
+%                                         'Expire', #"Cancel package $num later"
+%                                         @_
+%                                       );
+%                       }
+%
+%sub svc_recharge_link  { svc_popup_link( 'misc/recharge_svc.html',
+%                                         'Recharge',
+%                                         'Recharge',
+%                                         @_
+%                                       );
+%                       }
+%
+%sub pkg_link {
+%  my($action, $label, $cust_pkg) = @_;
+%  return '' unless $cust_pkg;
+%  qq!<a href="${p}misc/$action.cgi?!. $cust_pkg->pkgnum. qq!">$label</a>!;
 %}
 %
 %}
 %
-%sub pkg_suspend_link {
-%  my $pkg = shift or return '';
-%  qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('${p}misc/cancel_pkg.html?method=suspend&pkgnum=$pkg->{pkgnum}', 392, 336, 'suspend_pkg_popup' ), CAPTION, 'Suspend package $pkg->{pkgnum}', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Suspend</A>!;
+%sub pkg_popup_link {
+%  my($action, $label, $actionlabel, $cust_pkg) = @_;
+%  $action .= '&pkgnum='. $cust_pkg->pkgnum;
+%  $actionlabel .= ' package '. $cust_pkg->pkgnum;
+%  popup_link($action, $label, $actionlabel);
 %}
 %
 %}
 %
-%sub pkg_unsuspend_link {
-%  my $pkg = shift or return '';
-%  return qq!<a href="${p}misc/unsusp_pkg.cgi?$pkg->{pkgnum}">Unsuspend</a>!;
+%sub svc_popup_link {
+%  my($action, $label, $actionlabel, $cust_svc) = @_;
+%  $action .= '?svcnum='. $cust_svc->svcnum;
+%  $actionlabel .= ' service '. $cust_svc->svcnum;
+%  popup_link($action, $label, $actionlabel);
 %}
 %
 %}
 %
-%sub pkg_cancel_link {
-%  my $pkg = shift or return '';
-%
-%  qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('${p}misc/cancel_pkg.html?method=cancel&pkgnum=$pkg->{pkgnum}', 392, 336, 'cancel_pkg_popup' ), CAPTION, 'Cancel package $pkg->{pkgnum}', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Cancel now</A>!;
-%}
-%
-%sub pkg_expire_link {
-%  my $pkg = shift or return '';
-%  qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('${p}misc/cancel_pkg.html?method=expire&pkgnum=$pkg->{pkgnum}', 392, 336, 'expire_pkg_popup' ), CAPTION, 'Expire package $pkg->{pkgnum}', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">Cancel later</A>!;
-%}
-%
-%sub pkg_dates_link {
-%  my $pkg = shift or return '';
-%  qq!<A HREF="${p}edit/REAL_cust_pkg.cgi?$pkg->{pkgnum}">Edit&nbsp;dates</A>!;
+%sub popup_link {
+%  my($action, $label, $actionlabel) = @_;
+%  qq!<A HREF="javascript:void(0);" onClick="overlib( OLiframeContent('$p$action', 392, 336, 'pkg_or_svc_action_popup' ), CAPTION, '$actionlabel', STICKY, AUTOSTATUSCAP, MIDX, 0, MIDY, 0, DRAGGABLE, CLOSECLICK ); return false;">$label</A>!;
 %}
 %
 %sub pkg_customize_link {
 %}
 %
 %sub pkg_customize_link {
-%  my $pkg = shift or return '';
-%  my $custnum = shift;
-%  qq!<A HREF="${p}edit/part_pkg.cgi?keywords=$custnum;clone=$pkg->{pkgpart};!.
-%  qq!pkgnum=$pkg->{pkgnum}">Customize</A>!;
+%  my $cust_pkg = shift or return '';
+%  my $custnum = $cust_pkg->custnum;
+%  qq!<A HREF="${p}edit/part_pkg.cgi?!.
+%    "keywords=$custnum;".
+%    "clone=". $cust_pkg->part_pkg->pkgpart. ';'.
+%    "pkgnum=". $cust_pkg->pkgnum.
+%    qq!">Customize</A>!;
 %}
 %}
-%
-%
-
index 3543463..7b8df3a 100644 (file)
@@ -12,6 +12,9 @@
 %  # if not specified all columns (except for the primary key) will be viewable
 %  # 'fields' => [
 %  #             ]
 %  # if not specified all columns (except for the primary key) will be viewable
 %  # 'fields' => [
 %  #             ]
+%  #
+%  # # defaults to "edit/$table.cgi?", will have svcnum appended
+%  # 'edit_url' => 
 %
 %  my(%opt) = @_;
 %
 %
 %  my(%opt) = @_;
 %
 %               #|| [ grep { $_ ne 'svcnum' } dbdef->table($table)->columns ];
 %               || [ grep { $_ ne 'svcnum' } fields($table) ];
 %
 %               #|| [ grep { $_ ne 'svcnum' } dbdef->table($table)->columns ];
 %               || [ grep { $_ ne 'svcnum' } fields($table) ];
 %
-%  my($query) = $cgi->keywords;
-%  $query =~ /^(\d+)$/;
-%  my $svcnum = $1;
+%  my $svcnum;
+%  if ( $cgi->param('svcnum') ) {
+%    $cgi->param('svcnum') =~ /^(\d+)$/ or die "unparsable svcnum";
+%    $svcnum = $1;
+%  } else {
+%    my($query) = $cgi->keywords;
+%    $query =~ /^(\d+)$/ or die "no svcnum";
+%    $svcnum = $1;
+%  }
 %  my $svc_x = qsearchs( $opt{'table'}, { 'svcnum' => $svcnum } )
 %    or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
 %
 %  my $svc_x = qsearchs( $opt{'table'}, { 'svcnum' => $svcnum } )
 %    or die "Unknown svcnum $svcnum in ". $opt{'table'}. " table\n";
 %
@@ -69,7 +78,8 @@
 
 
 Service #<B><% $svcnum %></B>
 
 
 Service #<B><% $svcnum %></B>
-| <A HREF="<%$p%>edit/<% $opt{'table'} %>.cgi?<%$svcnum%>">Edit this <% $label %></A>
+% my $url = $opt{'edit_url'} || $p. 'edit/'. $opt{'table'}. '.cgi?';
+| <A HREF="<%$url%><%$svcnum%>">Edit this <% $label %></A>
 <BR>
 
 <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
 <BR>
 
 <% ntable("#cccccc") %><TR><TD><% ntable("#cccccc",2) %>
index 99b8da0..2fed8fc 100644 (file)
@@ -6,6 +6,6 @@
       }
     </SCRIPT>
 <input name="search_cust" accesskey="0" VALUE="(cust #, name, company or phone)" SIZE="28" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" STYLE="text-align:right; font-family: Arial, Verdana, Helvetica, sans-serif;"><BR>
       }
     </SCRIPT>
 <input name="search_cust" accesskey="0" VALUE="(cust #, name, company or phone)" SIZE="28" onFocus="clearhint_search_cust(this);" onClick="clearhint_search_cust(this);" STYLE="text-align:right; font-family: Arial, Verdana, Helvetica, sans-serif;"><BR>
-<A HREF="<% $RT::URI::freeside::URL %>/search/cust_main.html" STYLE="color: #000000; font-size: 70%; font-weight:normal">Advanced</A>
+<A NOTYET="<% $RT::URI::freeside::URL %>/search/cust_main.html" STYLE="color: #000000; font-size: 70%; font-weight:normal">Advanced</A>
 <input type="submit" value="<&|/l&>Search customers</&>" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
 </form>
 <input type="submit" value="<&|/l&>Search customers</&>" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
 </form>
index e9ad564..4a59424 100644 (file)
@@ -1,4 +1,4 @@
-<form action="<% $RT::URI::freeside::URL %>/search/svc_Smart.html" STYLE="margin:0">
+<form action="<% $RT::URI::freeside::URL %>/search/cust_svc.html" STYLE="margin:0">
     <SCRIPT TYPE="text/javascript">
       function clearhint_search_svc (what) {
         if ( what.value == '(user, user@domain or domain)' )
     <SCRIPT TYPE="text/javascript">
       function clearhint_search_svc (what) {
         if ( what.value == '(user, user@domain or domain)' )
@@ -6,6 +6,6 @@
       }
     </SCRIPT>
 <input name="search_svc" accesskey="0" VALUE="(user, user@domain or domain)" SIZE="26" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" STYLE="text-align:right; font-family: Arial, Verdana, Helvetica, sans-serif;"><BR>
       }
     </SCRIPT>
 <input name="search_svc" accesskey="0" VALUE="(user, user@domain or domain)" SIZE="26" onFocus="clearhint_search_svc(this);" onClick="clearhint_search_svc(this);" STYLE="text-align:right; font-family: Arial, Verdana, Helvetica, sans-serif;"><BR>
-            <A HREF="<% $RT::URI::freeside::URL %>search/svc_Smarter.html" STYLE="color: #000000; font-size: 70%; font-weight:normal">Advanced</A>
+            <A NOTYET="<% $RT::URI::freeside::URL %>search/svc_Smarter.html" STYLE="color: #000000; font-size: 70%; font-weight:normal">Advanced</A>
 <input type="submit" value="<&|/l&>Search services</&>" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
 </form>
 <input type="submit" value="<&|/l&>Search services</&>" CLASS="fsblackbutton" onMouseOver="this.className='fsblackbuttonselected'; return true;" onMouseOut="this.className='fsblackbutton'; return true;" STYLE="font-size:70%">
 </form>