Virtual field merge
authorkhoff <khoff>
Tue, 5 Aug 2003 00:20:51 +0000 (00:20 +0000)
committerkhoff <khoff>
Tue, 5 Aug 2003 00:20:51 +0000 (00:20 +0000)
73 files changed:
FS/FS/Record.pm
FS/FS/addr_block.pm
FS/FS/agent.pm
FS/FS/agent_type.pm
FS/FS/cust_bill.pm
FS/FS/cust_bill_event.pm
FS/FS/cust_bill_pay.pm
FS/FS/cust_bill_pkg.pm
FS/FS/cust_bill_pkg_detail.pm
FS/FS/cust_credit.pm
FS/FS/cust_credit_bill.pm
FS/FS/cust_credit_refund.pm
FS/FS/cust_main.pm
FS/FS/cust_main_county.pm
FS/FS/cust_main_invoice.pm
FS/FS/cust_pay.pm
FS/FS/cust_pay_batch.pm
FS/FS/cust_pkg.pm
FS/FS/cust_refund.pm
FS/FS/cust_svc.pm
FS/FS/cust_tax_exempt.pm
FS/FS/domain_record.pm
FS/FS/export_svc.pm
FS/FS/msgcat.pm
FS/FS/nas.pm
FS/FS/part_bill_event.pm
FS/FS/part_export.pm
FS/FS/part_export_option.pm
FS/FS/part_pkg.pm
FS/FS/part_pop_local.pm
FS/FS/part_referral.pm
FS/FS/part_svc.pm
FS/FS/part_svc_column.pm
FS/FS/part_virtual_field.pm [new file with mode: 0755]
FS/FS/pkg_svc.pm
FS/FS/port.pm
FS/FS/prepay_credit.pm
FS/FS/queue.pm
FS/FS/queue_arg.pm
FS/FS/queue_depend.pm
FS/FS/radius_usergroup.pm
FS/FS/router.pm
FS/FS/session.pm
FS/FS/svc_Common.pm
FS/FS/svc_acct.pm
FS/FS/svc_acct_pop.pm
FS/FS/svc_broadband.pm
FS/FS/svc_domain.pm
FS/FS/svc_forward.pm
FS/FS/svc_www.pm
FS/FS/type_pkgs.pm
FS/MANIFEST
FS/bin/freeside-setup
htetc/global.asa
htetc/handler.pl
htetc/handler.pl-1.0x
httemplate/browse/part_svc.cgi
httemplate/browse/part_virtual_field.cgi [new file with mode: 0644]
httemplate/browse/router.cgi
httemplate/docs/upgrade10.html
httemplate/edit/part_svc.cgi
httemplate/edit/part_virtual_field.cgi [new file with mode: 0644]
httemplate/edit/process/router.cgi
httemplate/edit/process/svc_broadband.cgi
httemplate/edit/router.cgi
httemplate/edit/svc_acct.cgi
httemplate/edit/svc_broadband.cgi
httemplate/edit/svc_www.cgi
httemplate/index.html
httemplate/view/svc_acct.cgi
httemplate/view/svc_broadband.cgi
httemplate/view/svc_forward.cgi
httemplate/view/svc_www.cgi

index 02fd4e3..9d82d94 100644 (file)
@@ -14,10 +14,14 @@ use FS::UID qw(dbh getotaker datasrc driver_name);
 use FS::SearchCache;
 use FS::Msgcat qw(gettext);
 
+use FS::part_virtual_field;
+
+use Tie::IxHash;
+
 @ISA = qw(Exporter);
 @EXPORT_OK = qw(dbh fields hfields qsearch qsearchs dbdef jsearch);
 
-$DEBUG = 0;
+$DEBUG = 2;
 $me = '[FS::Record]';
 
 #ask FS::UID to run this stuff for us later
@@ -200,12 +204,15 @@ sub qsearch {
   my $dbh = dbh;
 
   my $table = $cache ? $cache->table : $stable;
+  my $pkey = $dbdef->table($table)->primary_key;
 
-  my @fields = grep exists($record->{$_}), fields($table);
+  my @real_fields = grep exists($record->{$_}), real_fields($table);
+  my @virtual_fields = grep exists($record->{$_}), "FS::$table"->virtual_fields;
 
   my $statement = "SELECT $select FROM $stable";
-  if ( @fields ) {
-    $statement .= ' WHERE '. join(' AND ', map {
+  if ( @real_fields or @virtual_fields ) {
+    $statement .= ' WHERE '. join(' AND ',
+      ( map {
 
       my $op = '=';
       my $column = $_;
@@ -251,8 +258,45 @@ sub qsearch {
       } else {
         "$column $op ?";
       }
-    } @fields );
+    } @real_fields ), 
+    ( map {
+      my $op = '=';
+      my $column = $_;
+      if ( ref($record->{$_}) ) {
+        $op = $record->{$_}{'op'} if $record->{$_}{'op'};
+       if ( uc($op) eq 'ILIKE' ) {
+         $op = 'LIKE';
+         $record->{$_}{'value'} = lc($record->{$_}{'value'});
+         $column = "LOWER($_)";
+       }
+       $record->{$_} = $record->{$_}{'value'};
+      }
+
+      # ... EXISTS ( SELECT name, value FROM part_virtual_field
+      #              JOIN virtual_field
+      #              ON part_virtual_field.vfieldpart = virtual_field.vfieldpart
+      #              WHERE recnum = svc_acct.svcnum
+      #              AND (name, value) = ('egad', 'brain') )
+
+      my $value = $record->{$_};
+
+      my $subq;
+
+      $subq = ($value ? 'EXISTS ' : 'NOT EXISTS ') .
+      "( SELECT part_virtual_field.name, virtual_field.value ".
+      "FROM part_virtual_field JOIN virtual_field ".
+      "ON part_virtual_field.vfieldpart = virtual_field.vfieldpart ".
+      "WHERE virtual_field.recnum = ${table}.${pkey} ".
+      "AND part_virtual_field.name = '${column}'".
+      ($value ? 
+        " AND virtual_field.value ${op} '${value}'"
+      : "") . ")";
+      $subq;
+
+    } @virtual_fields ) );
+
   }
+
   $statement .= " $extra_sql" if defined($extra_sql);
 
   warn "[debug]$me $statement\n" if $DEBUG > 1;
@@ -262,7 +306,7 @@ sub qsearch {
   my $bind = 1;
 
   foreach my $field (
-    grep defined( $record->{$_} ) && $record->{$_} ne '', @fields
+    grep defined( $record->{$_} ) && $record->{$_} ne '', @real_fields
   ) {
     if ( $record->{$field} =~ /^\d+(\.\d+)?$/
          && $dbdef->table($table)->column($field)->type =~ /(int)/i
@@ -279,31 +323,64 @@ sub qsearch {
 
   $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
 
-  $dbh->commit or croak $dbh->errstr if $FS::UID::AutoCommit;
+  my %result;
+  tie %result, "Tie::IxHash";
+  @virtual_fields = "FS::$table"->virtual_fields;
+
+  my @stuff = @{ $sth->fetchall_arrayref( {} ) };
+  if($pkey) {
+    %result = map { $_->{$pkey}, $_ } @stuff;
+  } else {
+    @result{@stuff} = @stuff;
+  }
 
+  $sth->finish;
+  if ( keys(%result) and @virtual_fields ) {
+    $statement =
+      "SELECT virtual_field.recnum, part_virtual_field.name, ".
+             "virtual_field.value ".
+      "FROM part_virtual_field JOIN virtual_field USING (vfieldpart) ".
+      "WHERE part_virtual_field.dbtable = '$table' AND ".
+      "virtual_field.recnum IN (".
+      join(',', keys(%result)). ") AND part_virtual_field.name IN ('".
+      join(q!', '!, @virtual_fields) . "')";
+    warn "[debug]$me $statement\n" if $DEBUG > 1;
+    $sth = $dbh->prepare($statement) or croak "$dbh->errstr doing $statement";
+    $sth->execute or croak "Error executing \"$statement\": ". $sth->errstr;
+
+    foreach (@{ $sth->fetchall_arrayref({}) }) {
+      my $recnum = $_->{recnum};
+      my $name = $_->{name};
+      my $value = $_->{value};
+      if (exists($result{$recnum})) {
+        $result{$recnum}->{$name} = $value;
+      }
+    }
+  }
+  
   if ( eval 'scalar(@FS::'. $table. '::ISA);' ) {
     if ( eval 'FS::'. $table. '->can(\'new\')' eq \&new ) {
       #derivied class didn't override new method, so this optimization is safe
       if ( $cache ) {
         map {
           new_or_cached( "FS::$table", { %{$_} }, $cache )
-        } @{$sth->fetchall_arrayref( {} )};
+        } values(%result);
       } else {
         map {
           new( "FS::$table", { %{$_} } )
-        } @{$sth->fetchall_arrayref( {} )};
+        } values(%result);
       }
     } else {
       warn "untested code (class FS::$table uses custom new method)";
       map {
         eval 'FS::'. $table. '->new( { %{$_} } )';
-      } @{$sth->fetchall_arrayref( {} )};
+      } values(%result);
     }
   } else {
     cluck "warning: FS::$table not loaded; returning FS::Record objects";
     map {
       FS::Record->new( $table, { %{$_} } );
-    } @{$sth->fetchall_arrayref( {} )};
+    } values(%result);
   }
 
 }
@@ -512,15 +589,15 @@ sub insert {
 
   my $table = $self->table;
   #false laziness w/delete
-  my @fields =
+  my @real_fields =
     grep defined($self->getfield($_)) && $self->getfield($_) ne "",
-    $self->fields
+    real_fields($table)
   ;
-  my @values = map { _quote( $self->getfield($_), $table, $_) } @fields;
+  my @values = map { _quote( $self->getfield($_), $table, $_) } @real_fields;
   #eslaf
 
   my $statement = "INSERT INTO $table ( ".
-      join( ', ', @fields ).
+      join( ', ', @real_fields ).
     ") VALUES (".
       join( ', ', @values ).
     ")"
@@ -537,9 +614,9 @@ sub insert {
 
   $sth->execute or return $sth->errstr;
 
+  my $insertid = '';
   if ( $db_seq ) { # get inserted id from the database, if applicable
     warn "[debug]$me retreiving sequence from database\n" if $DEBUG;
-    my $insertid = '';
     if ( driver_name eq 'Pg' ) {
 
       my $oid = $sth->{'pg_oid_status'};
@@ -581,6 +658,34 @@ sub insert {
     $self->setfield($primary_key, $insertid);
   }
 
+  my @virtual_fields = 
+      grep defined($self->getfield($_)) && $self->getfield($_) ne "",
+          $self->virtual_fields;
+  if (@virtual_fields) {
+    my %v_values = map { $_, $self->getfield($_) } @virtual_fields;
+
+    my $vfieldpart = vfieldpart_hashref($table);
+
+    my $v_statement = "INSERT INTO virtual_field(recnum, vfieldpart, value) ".
+                    "VALUES (?, ?, ?)";
+
+    my $v_sth = dbh->prepare($v_statement) or do {
+      dbh->rollback if $FS::UID::AutoCommit;
+      return dbh->errstr;
+    };
+
+    foreach (keys(%v_values)) {
+      $v_sth->execute($self->getfield($primary_key),
+                      $vfieldpart->{$_},
+                      $v_values{$_})
+      or do {
+        dbh->rollback if $FS::UID::AutoCommit;
+        return $v_sth->errstr;
+      };
+    }
+  }
+
+
   my $h_sth;
   if ( defined $dbdef->table('h_'. $table) ) {
     my $h_statement = $self->_h_statement('insert');
@@ -631,7 +736,7 @@ sub delete {
         : "$_ = ". _quote($self->getfield($_),$self->table,$_)
     } ( $self->dbdef_table->primary_key )
           ? ( $self->dbdef_table->primary_key)
-          : $self->fields
+          : real_fields($self->table)
   );
   warn "[debug]$me $statement\n" if $DEBUG > 1;
   my $sth = dbh->prepare($statement) or return dbh->errstr;
@@ -645,6 +750,19 @@ sub delete {
     $h_sth = '';
   }
 
+  my $primary_key = $self->dbdef_table->primary_key;
+  my $v_sth;
+  my @del_vfields;
+  my $vfp = vfieldpart_hashref($self->table);
+  foreach($self->virtual_fields) {
+    next if $self->getfield($_) eq '';
+    unless(@del_vfields) {
+      my $st = "DELETE FROM virtual_field WHERE recnum = ? AND vfieldpart = ?";
+      $v_sth = dbh->prepare($st) or return dbh->errstr;
+    }
+    push @del_vfields, $_;
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -655,6 +773,10 @@ sub delete {
   my $rc = $sth->execute or return $sth->errstr;
   #not portable #return "Record not found, statement:\n$statement" if $rc eq "0E0";
   $h_sth->execute or return $h_sth->errstr if $h_sth;
+  $v_sth->execute($self->getfield($primary_key), $vfp->{$_}) 
+    or return $v_sth->errstr 
+        foreach (@del_vfields);
+  
   dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
 
   #no need to needlessly destoy the data either (causes problems actually)
@@ -695,8 +817,11 @@ sub replace {
   my $error = $new->check;
   return $error if $error;
 
-  my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields;
-  unless ( @diff ) {
+  #my @diff = grep $new->getfield($_) ne $old->getfield($_), $old->fields;
+  my %diff = map { ($new->getfield($_) ne $old->getfield($_))
+                   ? ($_, $new->getfield($_)) : () } $old->fields;
+                   
+  unless ( keys(%diff) ) {
     carp "[warning]$me $new -> replace $old: records identical";
     return '';
   }
@@ -704,7 +829,7 @@ sub replace {
   my $statement = "UPDATE ". $old->table. " SET ". join(', ',
     map {
       "$_ = ". _quote($new->getfield($_),$old->table,$_) 
-    } @diff
+    } real_fields($old->table)
   ). ' WHERE '.
     join(' AND ',
       map {
@@ -715,7 +840,7 @@ sub replace {
                 : "( $_ IS NULL OR $_ = \"\" )"
             )
           : "$_ = ". _quote($old->getfield($_),$old->table,$_)
-      } ( $primary_key ? ( $primary_key ) : $old->fields )
+      } ( $primary_key ? ( $primary_key ) : real_fields($old->table) )
     )
   ;
   warn "[debug]$me $statement\n" if $DEBUG > 1;
@@ -739,6 +864,44 @@ sub replace {
     $h_new_sth = '';
   }
 
+  # For virtual fields we have three cases with different SQL 
+  # statements: add, replace, delete
+  my $v_add_sth;
+  my $v_rep_sth;
+  my $v_del_sth;
+  my (@add_vfields, @rep_vfields, @del_vfields);
+  my $vfp = vfieldpart_hashref($old->table);
+  foreach(grep { exists($diff{$_}) } $new->virtual_fields) {
+    if($diff{$_} eq '') {
+      # Delete
+      unless(@del_vfields) {
+        my $st = "DELETE FROM virtual_field WHERE recnum = ? ".
+                 "AND vfieldpart = ?";
+        warn "[debug]$me $st\n" if $DEBUG > 2;
+        $v_del_sth = dbh->prepare($st) or return dbh->errstr;
+      }
+      push @del_vfields, $_;
+    } elsif($old->getfield($_) eq '') {
+      # Add
+      unless(@add_vfields) {
+        my $st = "INSERT INTO virtual_field (value, recnum, vfieldpart) ".
+                "VALUES (?, ?, ?)";
+        warn "[debug]$me $st\n" if $DEBUG > 2;
+        $v_add_sth = dbh->prepare($st) or return dbh->errstr;
+      }
+      push @add_vfields, $_;
+    } else {
+      # Replace
+      unless(@rep_vfields) {
+        my $st = "UPDATE virtual_field SET value = ? ".
+                 "WHERE recnum = ? AND vfieldpart = ?";
+        warn "[debug]$me $st\n" if $DEBUG > 2;
+        $v_rep_sth = dbh->prepare($st) or return dbh->errstr;
+      }
+      push @rep_vfields, $_;
+    }
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE'; 
@@ -750,6 +913,24 @@ sub replace {
   #not portable #return "Record not found (or records identical)." if $rc eq "0E0";
   $h_old_sth->execute or return $h_old_sth->errstr if $h_old_sth;
   $h_new_sth->execute or return $h_new_sth->errstr if $h_new_sth;
+
+  $v_del_sth->execute($old->getfield($primary_key),
+                      $vfp->{$_})
+        or return $v_del_sth->errstr
+      foreach(@del_vfields);
+
+  $v_add_sth->execute($new->getfield($_),
+                      $old->getfield($primary_key),
+                      $vfp->{$_})
+        or return $v_add_sth->errstr
+      foreach(@add_vfields);
+
+  $v_rep_sth->execute($new->getfield($_),
+                      $old->getfield($primary_key),
+                      $vfp->{$_})
+        or return $v_rep_sth->errstr
+      foreach(@rep_vfields);
+
   dbh->commit or croak dbh->errstr if $FS::UID::AutoCommit;
 
   '';
@@ -769,12 +950,28 @@ sub rep {
 
 =item check
 
-Not yet implemented, croaks.  Derived classes should provide a check method.
+Checks virtual fields (using check_blocks).  Subclasses should still provide 
+a check method to validate real fields, foreign keys, etc., and call this 
+method via $self->SUPER::check.
+
+(FIXME: Should this method try to make sure that it I<is> being called from 
+a subclass's check method, to keep the current semantics as far as possible?)
 
 =cut
 
 sub check {
-  confess "FS::Record::check not implemented; supply one in subclass!";
+  #confess "FS::Record::check not implemented; supply one in subclass!";
+  my $self = shift;
+
+  foreach my $field ($self->virtual_fields) {
+    for ($self->getfield($field)) {
+      # See notes on check_block in FS::part_virtual_field.
+      eval $self->pvf($field)->check_block;
+      return $@ if $@;
+      $self->setfield($field, $_);
+    }
+  }
+  '';
 }
 
 sub _h_statement {
@@ -782,7 +979,7 @@ sub _h_statement {
 
   my @fields =
     grep defined($self->getfield($_)) && $self->getfield($_) ne "",
-    $self->fields
+    real_fields($self->table);
   ;
   my @values = map { _quote( $self->getfield($_), $self->table, $_) } @fields;
 
@@ -1163,36 +1360,89 @@ sub ut_foreign_keyn {
     : '';
 }
 
+
+=item virtual_fields [ TABLE ]
+
+Returns a list of virtual fields defined for the table.  This should not 
+be exported, and should only be called as an instance or class method.
+
+=cut
+
+sub virtual_fields {
+  my $something = shift;
+  my $table;
+  $table = $something->table or confess "virtual_fields called on non-table";
+
+  confess "Unknown table $table" unless $dbdef->table($table);
+
+  # This should be smart enough to cache results.
+
+  my $query = 'SELECT name from part_virtual_field ' .
+              "WHERE dbtable = '$table'";
+  my $dbh = dbh;
+  my $result = $dbh->selectcol_arrayref($query);
+  confess $dbh->errstr if $dbh->err;
+  return @$result;
+}
+
+
 =item fields [ TABLE ]
 
-This can be used as both a subroutine and a method call.  It returns a list
-of the columns in this record's table, or an explicitly specified table.
-(See L<DBIx::DBSchema::Table>).
+This is a wrapper for real_fields and virtual_fields.  Code that called
+fields before should probably continue to call fields.
 
 =cut
 
-# Usage: @fields = fields($table);
-#        @fields = $record->fields;
 sub fields {
   my $something = shift;
   my $table;
-  if ( ref($something) ) {
+  if($something->isa('FS::Record')) {
     $table = $something->table;
   } else {
     $table = $something;
+    $something = "FS::$table";
   }
-  #croak "Usage: \@fields = fields(\$table)\n   or: \@fields = \$record->fields" unless $table;
-  my($table_obj) = $dbdef->table($table);
-  confess "Unknown table $table" unless $table_obj;
-  $table_obj->columns;
+  return (real_fields($table), $something->virtual_fields());
 }
 
 =back
 
+=item pvf FIELD_NAME
+
+Returns the FS::part_virtual_field object corresponding to a field in the 
+record (specified by FIELD_NAME).
+
+=cut
+
+sub pvf {
+  my ($self, $name) = (shift, shift);
+
+  if(grep /^$name$/, $self->virtual_fields) {
+    return qsearchs('part_virtual_field', { dbtable => $self->table,
+                                            name    => $name } );
+  }
+  ''
+}
+
 =head1 SUBROUTINES
 
 =over 4
 
+=item real_fields [ TABLE ]
+
+Returns a list of the real columns in the specified table.  Called only by 
+fields() and other subroutines elsewhere in FS::Record.
+
+=cut
+
+sub real_fields {
+  my $table = shift;
+
+  my($table_obj) = $dbdef->table($table);
+  confess "Unknown table $table" unless $table_obj;
+  $table_obj->columns;
+}
+
 =item reload_dbdef([FILENAME])
 
 Load a database definition (see L<DBIx::DBSchema>), optionally from a
@@ -1251,6 +1501,28 @@ sub _quote {
   }
 }
 
+=item vfieldpart_hashref TABLE
+
+Returns a hashref of virtual field names and vfieldparts applicable to the given
+TABLE.
+
+=cut
+
+sub vfieldpart_hashref {
+  my ($table) = @_;
+
+  return () unless $table;
+  my $dbh = dbh;
+  my $statement = "SELECT vfieldpart, name FROM part_virtual_field WHERE ".
+                  "dbtable = '$table'";
+  my $sth = $dbh->prepare($statement);
+  $sth->execute or croak "Execution of '$statement' failed: ".$dbh->errstr;
+  return { map { $_->{name}, $_->{vfieldpart} } 
+    @{$sth->fetchall_arrayref({})} };
+
+}
+
+
 =item hfields TABLE
 
 This is deprecated.  Don't use it.
index c5ddca7..4b034ef 100755 (executable)
@@ -122,7 +122,7 @@ sub check {
     }
   }
 
-  '';
+  $self->SUPER::check;
 }
 
 
index f11a28d..6de15ae 100644 (file)
@@ -113,8 +113,7 @@ sub check {
   return "Unknown typenum!"
     unless $self->agent_type;
 
-  '';
-
+  $self->SUPER::check;
 }
 
 =item agent_type
@@ -145,7 +144,7 @@ sub pkgpart_hashref {
 
 =head1 VERSION
 
-$Id: agent.pm,v 1.3 2002-03-24 18:23:47 ivan Exp $
+$Id: agent.pm,v 1.4 2003-08-05 00:20:40 khoff Exp $
 
 =head1 BUGS
 
index 988533a..5ba5ef2 100644 (file)
@@ -102,7 +102,8 @@ sub check {
   my $self = shift;
 
   $self->ut_numbern('typenum')
-  or $self->ut_text('atype');
+  or $self->ut_text('atype')
+  or $self->SUPER::check;
 
 }
 
@@ -150,7 +151,7 @@ sub pkgpart {
 
 =head1 VERSION
 
-$Id: agent_type.pm,v 1.1 1999-08-04 09:03:53 ivan Exp $
+$Id: agent_type.pm,v 1.2 2003-08-05 00:20:40 khoff Exp $
 
 =head1 BUGS
 
index a22f44b..4793608 100644 (file)
@@ -161,7 +161,7 @@ sub check {
 
   $self->printed(0) if $self->printed eq '';
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item previous
index c977347..ddd6762 100644 (file)
@@ -122,7 +122,7 @@ sub check {
   return "Unknown eventpart ". $self->eventpart
     unless qsearchs( 'part_bill_event' ,{ 'eventpart' => $self->eventpart } );
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item part_bill_event
index 913704b..5f4a491 100644 (file)
@@ -170,7 +170,7 @@ sub check {
 
   $self->_date(time) unless $self->_date;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cust_pay 
@@ -199,7 +199,7 @@ sub cust_bill {
 
 =head1 VERSION
 
-$Id: cust_bill_pay.pm,v 1.12 2002-02-07 22:29:34 ivan Exp $
+$Id: cust_bill_pay.pm,v 1.13 2003-08-05 00:20:41 khoff Exp $
 
 =head1 BUGS
 
index a6615d0..6800707 100644 (file)
@@ -171,7 +171,7 @@ sub check {
   return "Unknown invnum"
     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cust_pkg
index 199de43..261aa80 100644 (file)
@@ -105,7 +105,8 @@ sub check {
     || $self->ut_foreign_key('pkgnum', 'cust_pkg', 'pkgnum')
     || $self->ut_foreign_key('invnum', 'cust_pkg', 'invnum')
     || $self->ut_text('detail')
-  ;
+    || $self->SUPER::check
+    ;
 
 }
 
index 284d59d..e668abd 100644 (file)
@@ -174,7 +174,7 @@ sub check {
 
   $self->otaker(getotaker);
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cust_refund
@@ -242,7 +242,7 @@ sub credited {
 
 =head1 VERSION
 
-$Id: cust_credit.pm,v 1.16 2002-06-04 14:35:52 ivan Exp $
+$Id: cust_credit.pm,v 1.17 2003-08-05 00:20:41 khoff Exp $
 
 =head1 BUGS
 
index 6221541..a54acb6 100644 (file)
@@ -127,7 +127,7 @@ sub check {
   return "Cannot apply more than remaining value of invoice"
     unless $self->amount <= $cust_bill->owed;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item sub cust_credit
@@ -145,7 +145,7 @@ sub cust_credit {
 
 =head1 VERSION
 
-$Id: cust_credit_bill.pm,v 1.7 2002-01-24 16:58:47 ivan Exp $
+$Id: cust_credit_bill.pm,v 1.8 2003-08-05 00:20:41 khoff Exp $
 
 =head1 BUGS
 
index cc3b32c..d0deae2 100644 (file)
@@ -156,7 +156,7 @@ sub check {
   return "unknown cust_credit.crednum: ". $self->crednum
     unless qsearchs( 'cust_credit', { 'crednum' => $self->crednum } );
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cust_refund
@@ -185,7 +185,7 @@ sub cust_credit {
 
 =head1 VERSION
 
-$Id: cust_credit_refund.pm,v 1.9 2002-01-26 01:52:31 ivan Exp $
+$Id: cust_credit_refund.pm,v 1.10 2003-08-05 00:20:41 khoff Exp $
 
 =head1 BUGS
 
index 51037ef..2af2e98 100644 (file)
@@ -798,7 +798,7 @@ sub check {
 
   #warn "AFTER: \n". $self->_dump;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item all_pkgs
index d8796e4..f631d8c 100644 (file)
@@ -113,7 +113,9 @@ sub check {
     || $self->ut_textn('taxclass') # ...
     || $self->ut_money('exempt_amount')
     || $self->ut_textn('taxname')
-  ;
+    || $self->SUPER::check
+    ;
+
 
 }
 
index bcb1437..add0cca 100644 (file)
@@ -107,7 +107,7 @@ sub check {
   return "Unknown customer"
     unless qsearchs('cust_main',{ 'custnum' => $self->custnum });
 
-  ''; #noerror
+  $self->SUPER::check;
 }
 
 =item checkdest
@@ -163,7 +163,7 @@ sub address {
 
 =head1 VERSION
 
-$Id: cust_main_invoice.pm,v 1.13 2002-09-18 22:50:44 ivan Exp $
+$Id: cust_main_invoice.pm,v 1.14 2003-08-05 00:20:42 khoff Exp $
 
 =head1 BUGS
 
index 55f2fc4..7be1153 100644 (file)
@@ -354,8 +354,7 @@ sub check {
     return $error if $error;
   }
 
-  ''; #no error
-
+  $self->SUPER::check;
 }
 
 =item cust_bill_pay
@@ -390,7 +389,7 @@ sub unapplied {
 
 =head1 VERSION
 
-$Id: cust_pay.pm,v 1.24 2003-05-19 12:00:44 ivan Exp $
+$Id: cust_pay.pm,v 1.25 2003-08-05 00:20:42 khoff Exp $
 
 =head1 BUGS
 
index c4427c3..1a53046 100644 (file)
@@ -185,14 +185,14 @@ sub check {
 
   #check invnum, custnum, ?
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
 
 =head1 VERSION
 
-$Id: cust_pay_batch.pm,v 1.6 2002-02-22 23:08:11 ivan Exp $
+$Id: cust_pay_batch.pm,v 1.7 2003-08-05 00:20:42 khoff Exp $
 
 =head1 BUGS
 
index a423c55..f59b45a 100644 (file)
@@ -249,7 +249,7 @@ sub check {
     $self->manual_flag($1);
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cancel
index 7636717..250bd20 100644 (file)
@@ -260,14 +260,14 @@ sub check {
 
   $self->otaker(getotaker);
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
 
 =head1 VERSION
 
-$Id: cust_refund.pm,v 1.20 2002-11-19 09:51:58 ivan Exp $
+$Id: cust_refund.pm,v 1.21 2003-08-05 00:20:42 khoff Exp $
 
 =head1 BUGS
 
index c0cb6f4..7aa311b 100644 (file)
@@ -234,7 +234,7 @@ sub check {
       if scalar(@cust_svc) >= $quantity && (!$ignore_quantity || !$quantity);
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item part_svc
index ab873c0..da0de00 100644 (file)
@@ -111,6 +111,7 @@ sub check {
     || $self->ut_number('year') #check better
     || $self->ut_number('month') #check better
     || $self->ut_money('amount')
+    || $self->SUPER::check
   ;
 }
 
index 77b9550..ea0c48d 100644 (file)
@@ -271,7 +271,7 @@ sub check {
     die "ack!";
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item increment_serial
@@ -332,7 +332,7 @@ sub zone {
 
 =head1 VERSION
 
-$Id: domain_record.pm,v 1.15 2003-04-29 18:28:50 khoff Exp $
+$Id: domain_record.pm,v 1.16 2003-08-05 00:20:43 khoff Exp $
 
 =head1 BUGS
 
index da9ac69..c104e45 100644 (file)
@@ -105,6 +105,7 @@ sub check {
     || $self->ut_foreign_key('exportnum', 'part_export', 'exportnum')
     || $self->ut_number('svcpart')
     || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+    || $self->SUPER::check
   ;
 }
 
index fa10d34..855b8b2 100644 (file)
@@ -113,7 +113,7 @@ sub check {
   $self->locale =~ /^([\w\@]+)$/ or return "illegal locale: ". $self->locale;
   $self->locale($1);
 
-  ''; #no error
+  $self->SUPER::check
 }
 
 =back
index 58c6827..2d17df8 100644 (file)
@@ -114,7 +114,9 @@ sub check {
     || $self->ut_text('nas')
     || $self->ut_ip('nasip')
     || $self->ut_domain('nasfqdn')
-    || $self->ut_numbern('last');
+    || $self->ut_numbern('last')
+    || $self->SUPER::check
+    ;
 }
 
 =item heartbeat TIMESTAMP
@@ -136,7 +138,7 @@ sub heartbeat {
 
 =head1 VERSION
 
-$Id: nas.pm,v 1.6 2002-03-04 12:48:49 ivan Exp $
+$Id: nas.pm,v 1.7 2003-08-05 00:20:43 khoff Exp $
 
 =head1 BUGS
 
index e0e4f3f..9e5d821 100644 (file)
@@ -162,8 +162,7 @@ sub check {
     }
   }
 
-  '';
-
+  $self->SUPER::check;
 }
 
 =back
index ff51996..ab0a4b5 100644 (file)
@@ -285,7 +285,7 @@ sub check {
 
   #check exporttype?
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 #=item part_svc
@@ -667,6 +667,19 @@ END
   },
 ;
 
+tie my %router_options, 'Tie::IxHash',
+  'protocol' => {
+         label=>'Protocol',
+         type =>'select',
+         options => [qw(telnet ssh)],
+         default => 'telnet'},
+  'insert' => {label=>'Insert command', default=>'' },
+  'delete' => {label=>'Delete command', default=>'' },
+  'replace' => {label=>'Replace command', default=>'' },
+  'Timeout' => {label=>'Time to wait for prompt', default=>'20' },
+  'Prompt' => {label=>'Prompt string', default=>'#' }
+;
+
 tie my %domain_shellcommands_options, 'Tie::IxHash',
   'user' => { lable=>'Remote username', default=>'root' },
   'useradd' => { label=>'Insert command',
@@ -1013,8 +1026,12 @@ tie my %forward_shellcommands_options, 'Tie::IxHash',
   },
 
   'svc_broadband' => {
+    'router' => {
+      'desc' => 'Send a command to a router.',
+      'options' => \%router_options,
+      'notes' => '',
+    },
   },
-
 );
 
 =back
index a0b19fd..33b5e5a 100644 (file)
@@ -115,7 +115,7 @@ sub check {
 
   #check options & values?
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
index 6525864..12ee804 100644 (file)
@@ -241,6 +241,7 @@ sub check {
       || $self->ut_enum('recurtax', [ '', 'Y' ] )
       || $self->ut_textn('taxclass')
       || $self->ut_enum('disabled', [ '', 'Y' ] )
+      || $self->SUPER::check
     ;
 }
 
index 0b7cdf6..f7d5eac 100644 (file)
@@ -92,6 +92,7 @@ sub check {
       or $self->ut_text('state')
       or $self->ut_number('npa')
       or $self->ut_number('nxx')
+      or $self->SUPER::check
   ;
 
 }
@@ -100,7 +101,7 @@ sub check {
 
 =head1 VERSION
 
-$Id: part_pop_local.pm,v 1.1 2001-09-26 09:17:06 ivan Exp $
+$Id: part_pop_local.pm,v 1.2 2003-08-05 00:20:44 khoff Exp $
 
 =head1 BUGS
 
index 23885df..f30ddad 100644 (file)
@@ -93,6 +93,7 @@ sub check {
 
   $self->ut_numbern('refnum')
     || $self->ut_text('referral')
+    || $self->SUPER::check
   ;
 }
 
index 63bc2ad..aacc3ab 100644 (file)
@@ -68,7 +68,7 @@ TODOC:
 
 =item I<svcdb>__I<field> - Default or fixed value for I<field> in I<svcdb>.
 
-=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed
+=item I<svcdb>__I<field>_flag - defines I<svcdb>__I<field> action: null, `D' for default, or `F' for fixed.  For virtual fields, can also be 'X' for excluded.
 
 TODOC: EXTRA_FIELDS_ARRAYREF
 
@@ -113,7 +113,7 @@ sub insert {
     } );
 
     my $flag = $self->getfield($svcdb.'__'.$field.'_flag');
-    if ( uc($flag) =~ /^([DF])$/ ) {
+    if ( uc($flag) =~ /^([DFX])$/ ) {
       $part_svc_column->setfield('columnflag', $1);
       $part_svc_column->setfield('columnvalue',
         $self->getfield($svcdb.'__'.$field)
@@ -201,7 +201,7 @@ sub replace {
       } );
 
       my $flag = $new->getfield($svcdb.'__'.$field.'_flag');
-      if ( uc($flag) =~ /^([DF])$/ ) {
+      if ( uc($flag) =~ /^([DFX])$/ ) {
         $part_svc_column->setfield('columnflag', $1);
         $part_svc_column->setfield('columnvalue',
           $new->getfield($svcdb.'__'.$field)
@@ -254,7 +254,7 @@ sub check {
   my @fields = eval { fields( $recref->{svcdb} ) }; #might die
   return "Unknown svcdb!" unless @fields;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item part_svc_column COLUMNNAME
index 37e841e..885155b 100644 (file)
@@ -41,7 +41,7 @@ fields are currently supported:
 
 =item columnvalue - default or fixed value for the column
 
-=item columnflag - null, D or F
+=item columnflag - null, D, F, X (virtual fields)
 
 =back
 
@@ -91,18 +91,18 @@ sub check {
   ;
   return $error if $error;
 
-  $self->columnflag =~ /^([DF])$/
+  $self->columnflag =~ /^([DFX])$/
     or return "illegal columnflag ". $self->columnflag;
   $self->columnflag(uc($1));
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
 
 =head1 VERSION
 
-$Id: part_svc_column.pm,v 1.1 2001-09-07 20:49:15 ivan Exp $
+$Id: part_svc_column.pm,v 1.2 2003-08-05 00:20:44 khoff Exp $
 
 =head1 BUGS
 
diff --git a/FS/FS/part_virtual_field.pm b/FS/FS/part_virtual_field.pm
new file mode 100755 (executable)
index 0000000..03c34cc
--- /dev/null
@@ -0,0 +1,303 @@
+package FS::part_virtual_field;
+
+use strict;
+use vars qw( @ISA );
+use FS::Record qw( qsearchs qsearch dbdef );
+
+@ISA = qw( FS::Record );
+
+=head1 NAME
+
+FS::part_virtual_field - Object methods for part_virtual_field records
+
+=head1 SYNOPSIS
+
+  use FS::part_virtual_field;
+
+  $record = new FS::part_virtual_field \%hash;
+  $record = new FS::part_virtual_field { 'column' => 'value' };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_virtual_field object represents the definition of a virtual field 
+(see the BACKGROUND section).  FS::part_virtual_field contains the name and 
+base table of the field, as well as validation rules and UI hints about the 
+display of the field.  The actual data is stored in FS::virtual_field; see 
+its manpage for details.
+
+FS::part_virtual_field inherits from FS::Record.  The following fields are 
+currently supported:
+
+=over 2
+
+=item vfieldpart - primary key (assigned automatically)
+
+=item name - name of the field
+
+=item dbtable - table for which this virtual field is defined
+
+=item check_block - Perl code to validate/normalize data
+
+=item list_source - Perl code to generate a list of values (UI hint)
+
+=item length - expected length of the value (UI hint)
+
+=item label - descriptive label for the field (UI hint)
+
+=item sequence - sort key (UI hint; unimplemented)
+
+=back
+
+=head1 BACKGROUND
+
+"Form is none other than emptiness,
+ and emptiness is none other than form."
+-- Heart Sutra
+
+The virtual field mechanism allows site admins to make trivial changes to 
+the Freeside database schema without modifying the code.  Specifically, the 
+user can add custom-defined 'fields' to the set of data tracked by Freeside 
+about objects such as customers and services.  These fields are not associated 
+with any logic in the core Freeside system, but may be referenced in peripheral 
+code such as exports, price calculations, or alternate interfaces, or may just 
+be stored in the database for future reference.
+
+This system was originally devised for svc_broadband, which (by necessity) 
+comprises such a wide range of access technologies that no static set of fields 
+could contain all the information needed by the exports.  In an appalling 
+display of False Laziness, a parallel mechanism was implemented for the 
+router table, to store properties such as passwords to configure routers.
+
+The original system treated svc_broadband custom fields (sb_fields) as records 
+in a completely separate table.  Any code that accessed or manipulated these 
+fields had to be aware that they were I<not> fields in svc_broadband, but 
+records in sb_field.  For example, code that inserted a svc_broadband with 
+several custom fields had to create an FS::svc_broadband object, call its 
+insert() method, and then create several FS::sb_field objects and call I<their>
+insert() methods.
+
+This created a problem for exports.  The insert method on any FS::svc_Common 
+object (including svc_broadband) automatically triggers exports after the 
+record has been inserted.  However, at this point, the sb_fields had not yet 
+been inserted, so the export could not rely on their presence, which was the 
+original purpose of sb_fields.
+
+Hence the new system.  Virtual fields are appended to the field list of every 
+record at the FS::Record level, whether the object is created ex nihilo with 
+new() or fetched with qsearch().  The fields() method now returns a list of 
+both real and virtual fields.  The insert(), replace(), and delete() methods 
+now update both the base table and the virtual fields, in a single transaction.
+
+A new method is provided, virtual_fields(), which gives only the virtual 
+fields.  UI code that dynamically generates form widgets to edit virtual field
+data should use this to figure out what fields are defined.  (See below.)
+
+Subclasses may override virtual_fields() to restrict the set of virtual 
+fields available.  Some discipline and sanity on the part of the programmer 
+are required; in particular, this function should probably not depend on any 
+fields in the record other than the primary key, since the others may change 
+after the object is instantiated.  (Making it depend on I<virtual> fields is 
+just asking for pain.)  One use of this is seen in FS::svc_Common; another 
+possibility is field-level access control based on FS::UID::getotaker().
+
+As a trivial case, a subclass may opt out of supporting virtual fields with 
+the following code:
+
+sub virtual_fields { () }
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Create a new record.  To add the record to the database, see "insert".
+
+=cut
+
+sub table { 'part_virtual_field'; }
+sub virtual_fields { () }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=item delete
+
+Deletes this record from the database.  If there is an error, returns the
+error, otherwise returns false.
+
+=item replace OLD_RECORD
+
+Replaces OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=item check
+
+If there is an error, returns the error, otherwise returns false.
+Called by the insert and replace methods.
+
+=back
+
+=cut
+
+sub check {
+  my $self = shift;
+
+  my $error = $self->ut_text('name') ||
+              $self->ut_text('dbtable') ||
+              $self->ut_number('length')
+              ;
+  return $error if $error;
+
+  # Make sure it's a real table with a numeric primary key
+  my ($table, $pkey);
+  if($table = $FS::Record::dbdef->table($self->dbtable)) {
+    if($pkey = $table->primary_key) {
+      if($table->column($pkey)->type =~ /int/i) {
+        # this is what it should be
+      } else {
+        $error = "$table.$pkey is not an integer";
+      }
+    } else {
+      $error = "$table does not have a single-field primary key";
+    }
+  } else {
+    $error = "$table does not exist in the schema";
+  }
+  return $error if $error;
+
+  # Possibly some sanity checks for check_block and list_source?
+
+  $self->SUPER::check;  
+}
+
+=item list
+
+Evaluates list_source.
+
+=cut
+
+sub list {
+  my $self = shift;
+  return () unless $self->list_source;
+
+  my @opts = eval($self->list_source);
+  if($@) {
+    warn $@;
+    return ();
+  } else {
+    return @opts;
+  }
+}
+
+=item widget UI_TYPE MODE [ VALUE ]
+
+Generates UI code for a widget suitable for editing/viewing the field, based on 
+list_source and length.  
+
+The only UI_TYPE currently supported is 'HTML', and the only MODE is 'view'.
+Others will be added later.
+
+In HTML, all widgets are assumed to be table rows.  View widgets look like
+<TR><TD ALIGN="right">Label</TD><TD BGCOLOR="#ffffff">Value</TD></TR>
+
+(Most of the display style stuff, such as the colors, should probably go into 
+a separate module specific to the UI.  That can wait, though.  The API for 
+this function won't change.)
+
+VALUE (optional) is the current value of the field.
+
+=cut
+
+sub widget {
+  my $self = shift;
+  my ($ui_type, $mode, $value) = @_;
+  my $text;
+  my $label = $self->label || $self->name;
+
+  if ($ui_type eq 'HTML') {
+    if ($mode eq 'view') {
+      $text = q!<TR><TD ALIGN="right">! . $label . 
+              q!</TD><TD BGCOLOR="#ffffff">! . $value .
+              q!</TD></TR>! . "\n";
+    } elsif ($mode eq 'edit') {
+      $text = q!<TR><TD ALIGN="right">! . $label .
+              q!</TD><TD>!;
+      if ($self->list_source) {
+        $text .= q!<SELECT NAME="! . $self->name . 
+                q!" SIZE=1>! . "\n";
+        foreach ($self->list) {
+          $text .= q!<OPTION VALUE="! . $_ . q!"!;
+          $text .= ' SELECTED' if ($_ eq $value);
+          $text .= '>' . $_ . '</OPTION>' . "\n";
+        }
+      } else {
+        $text .= q!<INPUT NAME="! . $self->name .
+                q!" VALUE="! . $value . q!"!;
+        if ($self->length) {
+          $text .= q! SIZE="! . $self->length . q!"!;
+        }
+        $text .= '>';
+      }
+      $text .= q!</TD></TR>! . "\n";
+    } else {
+      return '';
+    }
+  } else {
+    return '';
+  }
+  return $text;
+}
+
+=head1 VERSION
+
+$Id: part_virtual_field.pm,v 1.2 2003-08-05 00:20:45 khoff Exp $
+
+=head1 NOTES
+
+=head2 Semantics of check_block:
+
+This has been changed from the sb_field implementation to make check_blocks 
+simpler and more natural to Perl programmers who work on things other than 
+Freeside.
+
+The check_block is eval'd with the (proposed) new value of the field in $_, 
+and the object to be updated in $self.  Its return value is ignored.  The 
+check_block may change the value of $_ to override the proposed value, or 
+call die() (with an appropriate error message) to reject the update entirely;
+the error string will be returned as the output of the check() method.
+
+This makes check_blocks like
+
+C<s/foo/bar/>
+
+do what you expect.
+
+The check_block is expected NOT to do anything freaky to $self, like modifying 
+other fields or calling $self->check().  You have been warned.
+
+(FIXME: Rewrite some of the warnings from part_sb_field and insert here.)
+
+=head1 BUGS
+
+None.  It's absolutely falwless.
+
+=head1 SEE ALSO
+
+L<FS::Record>, L<FS::virtual_field>
+
+=cut
+
+1;
+
+
index 3c544ff..2ac1a55 100644 (file)
@@ -108,7 +108,7 @@ sub check {
   return "Unknown pkgpart!" unless $self->part_pkg;
   return "Unknown svcpart!" unless $self->part_svc;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item part_pkg
@@ -137,7 +137,7 @@ sub part_svc {
 
 =head1 VERSION
 
-$Id: pkg_svc.pm,v 1.3 2002-06-10 01:39:50 khoff Exp $
+$Id: pkg_svc.pm,v 1.4 2003-08-05 00:20:45 khoff Exp $
 
 =head1 BUGS
 
index 13455ca..620030a 100644 (file)
@@ -113,7 +113,7 @@ sub check {
     unless $self->ip || $self->nasport;
   return "Unknown nasnum"
     unless qsearchs('nas', { 'nasnum' => $self->nasnum } );
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item session
@@ -133,7 +133,7 @@ sub session {
 
 =head1 VERSION
 
-$Id: port.pm,v 1.5 2001-02-14 04:33:06 ivan Exp $
+$Id: port.pm,v 1.6 2003-08-05 00:20:45 khoff Exp $
 
 =head1 BUGS
 
index 7ed9b83..a9d26d1 100644 (file)
@@ -108,6 +108,7 @@ sub check {
   || $self->ut_alpha('identifier')
   || $self->ut_money('amount')
   || $self->utnumbern('seconds')
+  || $self->SUPER::check
   ;
 
 }
index d35dc88..634f7f4 100644 (file)
@@ -207,7 +207,7 @@ sub check {
   $self->status('new') unless $self->status;
   $self->_date(time) unless $self->_date;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item args
@@ -385,7 +385,7 @@ END
 
 =head1 VERSION
 
-$Id: queue.pm,v 1.15 2002-07-02 06:48:59 ivan Exp $
+$Id: queue.pm,v 1.16 2003-08-05 00:20:46 khoff Exp $
 
 =head1 BUGS
 
index 08fe473..d23ee2a 100644 (file)
@@ -100,14 +100,14 @@ sub check {
   ;
   return $error if $error;
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =back
 
 =head1 VERSION
 
-$Id: queue_arg.pm,v 1.1 2001-09-11 00:08:18 ivan Exp $
+$Id: queue_arg.pm,v 1.2 2003-08-05 00:20:46 khoff Exp $
 
 =head1 BUGS
 
index 4a4e3c5..bc910d8 100644 (file)
@@ -103,6 +103,7 @@ sub check {
   $self->ut_numbern('dependnum')
     || $self->ut_foreign_key('jobnum',        'queue', 'jobnum')
     || $self->ut_foreign_key('depend_jobnum', 'queue', 'jobnum')
+    || $self->SUPER::check
   ;
 }
 
index 647621d..9bba057 100644 (file)
@@ -100,6 +100,7 @@ sub check {
     || $self->ut_number('svcnum')
     || $self->ut_foreign_key('svcnum','svc_acct','svcnum')
     || $self->ut_text('groupname')
+    || $self->SUPER::check
   ;
 }
 
index 3f9459a..2554ce8 100755 (executable)
@@ -85,7 +85,7 @@ sub check {
     || $self->ut_text('routername');
   return $error if $error;
 
-  '';
+  $self->SUPER::check;
 }
 
 =item addr_block
@@ -100,18 +100,6 @@ sub addr_block {
   return qsearch('addr_block', { routernum => $self->routernum });
 }
 
-=item router_field
-
-Returns a list of FS::router_field objects assigned to this object.
-
-=cut
-
-sub router_field {
-  my $self = shift;
-
-  return qsearch('router_field', { routernum => $self->routernum });
-}
-
 =item part_svc_router
 
 Returns a list of FS::part_svc_router objects associated with this 
@@ -147,7 +135,7 @@ $Id:
 
 =head1 SEE ALSO
 
-FS::svc_broadband, FS::router, FS::addr_block, FS::router_field, FS::part_svc,
+FS::svc_broadband, FS::router, FS::addr_block, FS::part_svc,
 schema.html from the base documentation.
 
 =cut
index de0f2a7..2ad594c 100644 (file)
@@ -216,7 +216,7 @@ sub check {
   return $error if $error;
   return "Unknown svcnum"
     unless qsearchs('svc_acct', { 'svcnum' => $self->svcnum } );
-  '';
+  $self->SUPER::check;
 }
 
 =item nas_heartbeat
@@ -247,7 +247,7 @@ sub svc_acct {
 
 =head1 VERSION
 
-$Id: session.pm,v 1.7 2001-04-15 13:35:12 ivan Exp $
+$Id: session.pm,v 1.8 2003-08-05 00:20:46 khoff Exp $
 
 =head1 BUGS
 
index 87b6097..524e550 100644 (file)
@@ -2,7 +2,7 @@ package FS::svc_Common;
 
 use strict;
 use vars qw( @ISA $noexport_hack );
-use FS::Record qw( qsearchs fields dbh );
+use FS::Record qw( qsearch qsearchs fields dbh );
 use FS::cust_svc;
 use FS::part_svc;
 use FS::queue;
@@ -28,6 +28,58 @@ inherit from, i.e. FS::svc_acct.  FS::svc_Common inherits from FS::Record.
 
 =over 4
 
+=cut
+
+sub virtual_fields {
+
+  # This restricts the fields based on part_svc_column and the svcpart of 
+  # the service.  There are four possible cases:
+  # 1.  svcpart passed as part of the svc_x hash.
+  # 2.  svcpart fetched via cust_svc based on svcnum.
+  # 3.  No svcnum or svcpart.  In this case, return ALL the fields with 
+  #     dbtable eq $self->table.
+  # 4.  Called via "fields('svc_acct')" or something similar.  In this case
+  #     there is no $self object.
+
+  my $self = shift;
+  my $svcpart;
+  my @vfields = $self->SUPER::virtual_fields;
+
+  return @vfields unless (ref $self); # Case 4
+
+  if ($self->svcpart) { # Case 1
+    $svcpart = $self->svcpart;
+  } elsif (my $cust_svc = $self->cust_svc) { # Case 2
+    $svcpart = $cust_svc->svcpart;
+  } else { # Case 3
+    $svcpart = '';
+  }
+
+  if ($svcpart) { #Cases 1 and 2
+    my %flags = map { $_->columnname, $_->columnflag } (
+        qsearch ('part_svc_column', { svcpart => $svcpart } )
+      );
+    return grep { not ($flags{$_} eq 'X') } @vfields;
+  } else { # Case 3
+    return @vfields;
+  } 
+  return ();
+}
+
+=item check
+
+Checks the validity of fields in this record.
+
+At present, this does nothing but call FS::Record::check (which, in turn, 
+does nothing but run virtual field checks).
+
+=cut
+
+sub check {
+  my $self = shift;
+  $self->SUPER::check;
+}
+
 =item insert [ JOBNUM_ARRAYREF ]
 
 Adds this record to the database.  If there is an error, returns the error,
@@ -254,7 +306,7 @@ sub setx {
 
   #set default/fixed/whatever fields from part_svc
   my $table = $self->table;
-  foreach my $field ( grep { $_ ne 'svcnum' } fields($table) ) {
+  foreach my $field ( grep { $_ ne 'svcnum' } $self->fields ) {
     my $part_svc_column = $part_svc->part_svc_column($field);
     if ( $part_svc_column->columnflag eq $x ) {
       $self->setfield( $field, $part_svc_column->columnvalue );
@@ -364,7 +416,7 @@ sub cancel { ''; }
 
 =head1 VERSION
 
-$Id: svc_Common.pm,v 1.12 2002-06-14 11:22:53 ivan Exp $
+$Id: svc_Common.pm,v 1.13 2003-08-05 00:20:47 khoff Exp $
 
 =head1 BUGS
 
index 8a6f2c4..0ee7a72 100644 (file)
@@ -832,7 +832,7 @@ sub check {
            ": ". $recref->{_password};
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item radius
index 196ab7e..f98f91a 100644 (file)
@@ -93,6 +93,7 @@ sub check {
       or $self->ut_number('ac')
       or $self->ut_number('exch')
       or $self->ut_numbern('loc')
+      or $self->SUPER::check
   ;
 
 }
@@ -187,7 +188,7 @@ END
 
 =head1 VERSION
 
-$Id: svc_acct_pop.pm,v 1.9 2003-07-04 01:37:46 ivan Exp $
+$Id: svc_acct_pop.pm,v 1.10 2003-08-05 00:20:47 khoff Exp $
 
 =head1 BUGS
 
index 45f6c36..ec91532 100755 (executable)
@@ -116,50 +116,6 @@ returns the error, otherwise returns false.
 
 # Standard FS::svc_Common::replace
 
-=item sb_field
-
-Returns a list of FS::sb_field objects assigned to this object.
-
-=cut
-
-sub sb_field {
-  my $self = shift;
-
-  return qsearch( 'sb_field', { svcnum => $self->svcnum } );
-}
-
-=item sb_field_hashref
-
-Returns a hashref of the FS::sb_field key/value pairs for this object.
-
-Deprecated.  Please don't use it.
-
-=cut
-
-# Kristian wrote this, but don't hold it against him.  He was under a powerful
-# distracting influence whom he evidently found much more interesting than
-# svc_broadband.pm.  I can't say I blame him.
-
-sub sb_field_hashref {
-  my $self = shift;
-  my $svcpart = shift;
-
-  if ((not $svcpart) && ($self->cust_svc)) {
-    $svcpart = $self->cust_svc->svcpart;
-  }
-
-  my $hashref = {};
-
-  map {
-    my $sb_field = qsearchs('sb_field', { sbfieldpart => $_->sbfieldpart,
-                                          svcnum => $self->svcnum });
-    $hashref->{$_->getfield('name')} = $sb_field ? $sb_field->getfield('value') : '';
-  } qsearch('part_sb_field', { svcpart => $svcpart });
-
-  return $hashref;
-
-}
-
 =item suspend
 
 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
@@ -223,8 +179,7 @@ sub check {
     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
   }
 
-
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item NetAddr
@@ -267,19 +222,11 @@ sub allowed_routers {
 
 =head1 BUGS
 
-I think there's one place in the code where we actually use sb_field_hashref.
-That's a bug in itself.
-
-The real problem with it is that we're still grappling with the question of how
-tightly xfields should be integrated with real fields.  There are a few
-different directions we could go with it--we I<could> override several
-functions in Record so that xfields behave almost exactly like real fields (can
-be set with setfield(), appear in fields() and hash(), used as criteria in
-qsearch(), etc.).
+The business with sb_field has been 'fixed', in a manner of speaking.
 
 =head1 SEE ALSO
 
-FS::svc_Common, FS::Record, FS::addr_block, FS::sb_field,
+FS::svc_Common, FS::Record, FS::addr_block,
 FS::part_svc, schema.html from the base documentation.
 
 =cut
index 32b9456..ff0fa2f 100644 (file)
@@ -342,7 +342,9 @@ sub check {
     return "Unknown catchall" unless $svc_acct;
   }
 
-  $self->ut_textn('purpose');
+  my $error = $self->ut_textn('purpose')
+           or $self->SUPER::check;
+  return $error if $error;
 
 }
 
index 2b1fb92..7a121b8 100644 (file)
@@ -241,7 +241,7 @@ sub check {
     $self->dst('');
   }
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item srcsvc_acct
index d7a42c8..7e89083 100644 (file)
@@ -234,7 +234,8 @@ sub check {
   return "Unknown usersvc (svc_acct.svcnum): ". $self->usersvc
     unless qsearchs('svc_acct', { 'svcnum' => $self->usersvc } );
 
-  ''; #no error
+  $self->SUPER::check;
+
 }
 
 =item domain_record
index efba60d..5b3b11c 100644 (file)
@@ -91,7 +91,7 @@ sub check {
   return "Unknown pkgpart"
     unless qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } );
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item part_pkg
@@ -111,7 +111,7 @@ sub part_pkg {
 
 =head1 VERSION
 
-$Id: type_pkgs.pm,v 1.2 2002-10-04 12:57:06 ivan Exp $
+$Id: type_pkgs.pm,v 1.3 2003-08-05 00:20:48 khoff Exp $
 
 =head1 BUGS
 
index 846f373..0fbbf5c 100644 (file)
@@ -92,9 +92,8 @@ FS/part_pop_local.pm
 FS/part_referral.pm
 FS/part_svc.pm
 FS/part_svc_column.pm
-FS/part_router_field.pm
-FS/part_sb_field.pm
 FS/part_svc_router.pm
+FS/part_virtual_field.pm
 FS/pkg_svc.pm
 FS/svc_Common.pm
 FS/svc_acct.pm
@@ -102,7 +101,6 @@ FS/svc_acct_pop.pm
 FS/svc_broadband.pm
 FS/svc_domain.pm
 FS/router.pm
-FS/router_field.pm
 FS/type_pkgs.pm
 FS/nas.pm
 FS/port.pm
@@ -111,7 +109,6 @@ FS/domain_record.pm
 FS/prepay_credit.pm
 FS/svc_www.pm
 FS/svc_forward.pm
-FS/sb_field.pm
 FS/raddb.pm
 FS/radius_usergroup.pm
 FS/queue.pm
index 734744e..2cb555e 100755 (executable)
@@ -1031,7 +1031,7 @@ sub tables_hash_hack {
       'columns' => [
         'routernum', 'serial', '', '',
         'routername', 'varchar', '', $char_d,
-        'svcnum', 'int', '0', '',
+        'svcnum', 'int', 'NULL', '',
       ],
       'primary_key' => 'routernum',
       'unique'      => [],
@@ -1048,30 +1048,6 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
-    'part_router_field' => {
-      'columns' => [
-        'routerfieldpart', 'serial', '', '',
-        'name', 'varchar', '', $char_d,
-       'length', 'int', '', '',
-       'check_block', 'text', 'NULL', '',
-       'list_source', 'text', 'NULL', '',
-      ],
-      'primary_key' => 'routerfieldpart',
-      'unique'      => [],
-      'index'       => [],
-    },
-
-    'router_field' => {
-      'columns' => [
-        'routerfieldpart', 'int', '', '',
-        'routernum', 'int', '', '',
-        'value', 'varchar', '', 128,
-      ],
-      'primary_key' => '',
-      'unique'      => [ [ 'routerfieldpart', 'routernum' ] ],
-      'index'       => [],
-    },
-
     'addr_block' => {
       'columns' => [
         'blocknum', 'serial', '', '',
@@ -1084,31 +1060,6 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
-    'part_sb_field' => {
-      'columns' => [
-        'sbfieldpart', 'serial', '', '',
-       'svcpart', 'int', '', '',
-       'name', 'varchar', '', $char_d,
-       'length', 'int', '', '',
-       'check_block', 'text', 'NULL', '',
-       'list_source', 'text', 'NULL', '',
-      ],
-      'primary_key' => 'sbfieldpart',
-      'unique'      => [ [ 'sbfieldpart', 'svcpart' ] ],
-      'index'       => [],
-    },
-
-    'sb_field' => {
-      'columns' => [
-        'sbfieldpart', 'int', '', '',
-       'svcnum', 'int', '', '',
-       'value', 'varchar', '', 128,
-      ],
-      'primary_key' => '',
-      'unique'      => [ [ 'sbfieldpart', 'svcnum' ] ],
-      'index'       => [],
-    },
-
     'svc_broadband' => {
       'columns' => [
         'svcnum', 'int', '', '',
@@ -1122,6 +1073,32 @@ sub tables_hash_hack {
       'index'       => [],
     },
 
+    'part_virtual_field' => {
+      'columns' => [
+        'vfieldpart', 'int', '', '',
+        'dbtable', 'varchar', '', 32,
+        'name', 'varchar', '', 32,
+        'check_block', 'text', 'NULL', '',
+        'length', 'int', 'NULL', '',
+        'list_source', 'text', 'NULL', '',
+        'label', 'varchar', 'NULL', 80,
+      ],
+      'primary_key' => 'vfieldpart',
+      'unique' => [],
+      'index' => [],
+    },
+
+    'virtual_field' => {
+      'columns' => [
+        'recnum', 'int', '', '',
+        'vfieldpart', 'int', '', '',
+        'value', 'varchar', '', 128,
+      ],
+      'primary_key' => '',
+      'unique' => [ [ 'vfieldpart', 'recnum' ] ],
+      'index' => [],
+    },
+
   );
 
   %tables;
index d87f1ea..6f76fd2 100644 (file)
@@ -42,6 +42,7 @@ use FS::part_pkg;
 use FS::part_referral;
 use FS::part_svc;
 use FS::part_svc_router;
+use FS::part_virtual_field;
 use FS::pkg_svc;
 use FS::port;
 use FS::queue qw(joblisting);
@@ -53,11 +54,7 @@ use FS::svc_domain;
 use FS::svc_forward;
 use FS::svc_www;
 use FS::router;
-use FS::part_router_field;
-use FS::router_field;
 use FS::addr_block;
-use FS::part_sb_field;
-use FS::sb_field;
 use FS::svc_broadband;
 use FS::type_pkgs;
 use FS::part_export;
index 481d5a2..c81db86 100644 (file)
@@ -100,6 +100,7 @@ sub handler
       use FS::part_referral;
       use FS::part_svc;
       use FS::part_svc_router;
+      use FS::part_virtual_field;
       use FS::pkg_svc;
       use FS::port;
       use FS::queue qw(joblisting);
@@ -112,11 +113,7 @@ sub handler
       use FS::svc_forward;
       use FS::svc_www;
       use FS::router;
-      use FS::part_router_field;
-      use FS::router_field;
       use FS::addr_block;
-      use FS::part_sb_field;
-      use FS::sb_field;
       use FS::svc_broadband;
       use FS::type_pkgs;
       use FS::part_export;
index 8840b08..def579f 100644 (file)
@@ -95,6 +95,7 @@ sub handler
       use FS::part_referral;
       use FS::part_svc;
       use FS::part_svc_router;
+      use FS::part_virtual_field;
       use FS::pkg_svc;
       use FS::port;
       use FS::queue qw(joblisting);
@@ -107,11 +108,7 @@ sub handler
       use FS::svc_forward;
       use FS::svc_www;
       use FS::router;
-      use FS::part_router_field;
-      use FS::router_field;
       use FS::addr_block;
-      use FS::part_sb_field;
-      use FS::sb_field;
       use FS::svc_broadband;
       use FS::type_pkgs;
       use FS::part_export;
index 7c83924..3fece29 100755 (executable)
@@ -53,12 +53,13 @@ function part_export_areyousure(href) {
 <% foreach my $part_svc ( @part_svc ) {
      my $hashref = $part_svc->hashref;
      my $svcdb = $hashref->{svcdb};
-     my @dfields = fields($svcdb);
+     my $svc_x = "FS::$svcdb"->new( { svcpart => $part_svc->svcpart } );
+     my @dfields = $svc_x->fields;
      push @dfields, 'usergroup' if $svcdb eq 'svc_acct'; #kludge
      my @fields =
-       grep { $_ ne 'svcnum' && $part_svc->part_svc_column($_)->columnflag }
-            @dfields;
-
+       grep { $svc_x->pvf($_)
+           or $_ ne 'svcnum' && $part_svc->part_svc_column($_)->columnflag }
+            @dfields ;
      my $rowspan = scalar(@fields) || 1;
      my $url = "${p}edit/part_svc.cgi?$hashref->{svcpart}";
 %>
@@ -96,6 +97,7 @@ map { qsearchs('part_export', { exportnum => $_->exportnum } ) } qsearch('export
 
 <%     if ( $flag eq "D" ) { print "Default"; }
          elsif ( $flag eq "F" ) { print "Fixed"; }
+         elsif ( not $flag ) { }
          else { print "(Unknown!)"; }
 %>
        </TD><TD><%= $part_svc->part_svc_column($field)->columnvalue%></TD>
diff --git a/httemplate/browse/part_virtual_field.cgi b/httemplate/browse/part_virtual_field.cgi
new file mode 100644 (file)
index 0000000..9bb5c13
--- /dev/null
@@ -0,0 +1,39 @@
+<%= header('Virtual field definitions', menubar('Main Menu'   => $p)) %>
+<%
+
+my %pvfs;
+my $block;
+my $p2 = popurl(2);
+my $dbtable;
+
+foreach (qsearch('part_virtual_field', {})) {
+  push @{ $pvfs{$_->dbtable} }, $_;
+}
+%>
+
+<% if ($cgi->param('error')) { %>
+   <FONT SIZE="+1" COLOR="#ff0000">Error: <%=$cgi->param('error')%></FONT>
+   <BR><BR>
+<% } %>
+
+<A HREF="<%=$p2%>edit/part_virtual_field.cgi"><I>Add a new field</I></A><BR><BR>
+
+<% foreach $dbtable (sort { $a cmp $b } keys (%pvfs)) { %>
+<H3><%=$dbtable%></H3>
+
+<%=table()%>
+<TH><TD>Field name</TD><TD>Description</TD></TH>
+<% foreach $pvf (sort {$a->name cmp $b->name} @{ $pvfs{$dbtable} }) { %>
+  <TR>
+    <TD></TD>
+    <TD>
+      <A HREF="<%=$p2%>edit/part_virtual_field.cgi?<%=$pvf->vfieldpart%>">
+        <%=$pvf->name%></A></TD>
+    <TD><%=$pvf->label%></TD>
+  </TR>
+<%   } %>
+</TABLE>
+<% } %>
+</BODY>
+</HTML>
+
index 8864936..a9ec6fd 100644 (file)
@@ -14,21 +14,22 @@ my $p2 = popurl(2);
 <A HREF="<%=$p2%>edit/router.cgi"><I>Add a new router</I></A><BR><BR>
 
 <%=table()%>
-<!-- <TH><TD>Field name</TD><TD>Field value</TD></TH> -->
-<% foreach $router (sort {$a->routernum <=> $b->routernum} @router) { %>
   <TR>
-<!--    <TD ROWSPAN="<%=scalar($router->router_field) + 2%>"> -->
-    <TD>
+    <TD><B>Router name</B></TD>
+    <TD><B>Address block(s)</B></TD>
+  </TR>
+<% foreach $router (sort {$a->routernum <=> $b->routernum} @router) {
+     my @addr_block = $router->addr_block;
+%>
+  <TR>
+    <TD ROWSPAN="<%=scalar(@addr_block)%>">
       <A HREF="<%=$p2%>edit/router.cgi?<%=$router->routernum%>"><%=$router->routername%></A>
     </TD>
-  <!-- 
-  <% foreach (sort { $a->part_router_field->name cmp $b->part_router_field->name } $router->router_field )  { %>
-  <TR>
-    <TD BGCOLOR="#cccccc" ALIGN="right"><%=$_->part_router_field->name%></TD>
-    <TD BGCOLOR="#ffffff"><%=$_->value%></TD>
-  </TR>
-  <% } %>
-  -->
+    <TD>
+    <% foreach my $block ( @addr_block ) { %>
+      <%=$block->NetAddr%></BR>
+    <% } %>
+    </TD>
   </TR>
 <% } %>
 </TABLE>
index 1035510..4c2c17b 100644 (file)
@@ -14,6 +14,24 @@ CREATE TABLE cust_bill_pkg_detail (
 );
 CREATE INDEX cust_bill_pkg_detail1 ON cust_bill_pkg_detail ( pkgnum, invnum );
 
+CREATE TABLE part_virtual_field (
+  vfieldpart int NOT NULL,
+  dbtable varchar(32) NOT NULL,
+  name varchar(32) NOT NULL,
+  check_block text,
+  list_source text,
+  length integer,
+  label varchar(80),
+  PRIMARY KEY (vfieldpart)
+);
+
+CREATE TABLE virtual_field (
+  recnum integer NOT NULL,
+  vfieldpart integer NOT NULL,
+  value varchar(128) NOT NULL,
+  PRIMARY KEY (vfieldpart, recnum)
+);
+
 CREATE TABLE router (
   routernum serial,
   routername varchar(80),
@@ -26,22 +44,6 @@ CREATE TABLE part_svc_router (
   routernum int NOT NULL
 );
 
-CREATE TABLE part_router_field (
-  routerfieldpart serial,
-  name varchar(80),
-  length int NOT NULL,
-  check_block text,
-  list_source text,
-  PRIMARY KEY (routerfieldpart)
-);
-
-CREATE TABLE router_field (
-  routerfieldpart int NOT NULL,
-  routernum int NOT NULL,
-  value varchar(128)
-);
-CREATE UNIQUE INDEX router_field1 ON router_field ( routerfieldpart, routernum );
-
 CREATE TABLE addr_block (
   blocknum serial,
   routernum int NOT NULL,
@@ -51,24 +53,6 @@ CREATE TABLE addr_block (
 );
 CREATE UNIQUE INDEX addr_block1 ON addr_block ( blocknum, routernum );
 
-CREATE TABLE part_sb_field (
-  sbfieldpart serial,
-  svcpart int NOT NULL,
-  name varchar(80) NOT NULL,
-  length int NOT NULL,
-  check_block text NULL,
-  list_source text NULL,
-  PRIMARY key (sbfieldpart)
-);
-CREATE UNIQUE INDEX part_sb_field1 ON part_sb_field ( sbfieldpart, svcpart );
-
-CREATE TABLE sb_field (
-  sbfieldpart int NOT NULL,
-  svcnum int NOT NULL,
-  value varchar(128)
-);
-CREATE UNIQUE INDEX sb_field1 ON sb_field ( sbfieldpart, svcnum );
-
 CREATE TABLE svc_broadband (
   svcnum int NOT NULL,
   blocknum int NOT NULL,
@@ -112,7 +96,7 @@ optionally:
 mandatory again:
 
 dbdef-create username
-create-history-tables username cust_bill_pkg_detail router part_svc_router part_router_field router_field addr_block part_sb_field sb_field svc_broadband
+create-history-tables username cust_bill_pkg_detail router part_svc_router addr_block svc_broadband
 dbdef-create username
 
 
index d4bb470..45e0b6f 100755 (executable)
@@ -65,6 +65,9 @@ blank <B>slipip</B> as well as a fixed shell something like <B>/bin/true</B> or
 <BR><BR>
 
 <%
+
+my %vfields;
+
 #these might belong somewhere else for other user interfaces 
 #pry need to eventually create stuff that's shared amount UIs
 my %defs = (
@@ -109,29 +112,43 @@ my %defs = (
     '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_charge' => {
+#    'amount'    => 'amount',
+#  },
+#  'svc_wo' => {
+#    'worker'    => 'Worker',
+#    '_date'      => 'Date',
+#  },
   'svc_www' => {
     #'recnum' => '',
     #'usersvc' => '',
   },
   'svc_broadband' => {
-    'actypenum' => 'This is the actypenum that refers to the type of AC that can be provisioned for this service.  This field must be set fixed.',
     '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.',
-    'acnum' => 'acnum of a specific AC that this service is restricted to.  Not required',
     'ip_addr' => 'IP address.  Leave blank for automatic assignment.',
-    'ip_netmask' => 'Mask length, aka. netmask bits.  (Eg. 255.255.255.0 == 24)',
-    'mac_addr' => 'MAC address which is used by some ACs for access control.  Specified by 6 colon seperated hex octets. (Eg. 00:00:0a:bc:1a:2b)',
-    'location' => 'Defines the physically location at which this service was installed.  This is not necessarily the billing address',
+    'blocknum' => 'Address block.',
   },
 );
 
+  foreach $svcdb (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
+  
   my @dbs = $hashref->{svcdb}
              ? ( $hashref->{svcdb} )
              : qw( svc_acct svc_domain svc_forward svc_www svc_broadband );
@@ -157,7 +174,7 @@ my %defs = (
       $html .= '<BR><BR>'. table().
                table(). "<TR><TH COLSPAN=$columns>Exports</TH></TR><TR>";
       foreach my $part_export ( @part_export ) {
-        $html .= '<TD><INPUT TYPE="checkbox"'.
+        $html .= '<TD><INPUT TYPE="checbox"'.
                  ' NAME="exportnum'. $part_export->exportnum. '"  VALUE="1" ';
         $html .= 'CHECKED'
           if ( $clone || $part_svc->svcpart ) #null svcpart search causing error
@@ -202,19 +219,27 @@ my %defs = (
             qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="D"!.
             ' CHECKED'x($flag eq 'D'). ">Default ".
             qq!<INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="F"!.
-            ' CHECKED'x($flag eq 'F'). ">Fixed ".
-            '<BR>';
+            ' CHECKED'x($flag eq 'F'). ">Fixed ";
+          $html .= '<BR>';
         }
         if ( ref($def) ) {
           if ( $def->{type} eq 'select' ) {
             $html .= qq!<SELECT NAME="${layer}__${field}">!;
             $html .= '<OPTION> </OPTION>' unless $value;
-            foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
-              my $rvalue = $record->getfield($def->{select_key});
-              $html .= qq!<OPTION VALUE="$rvalue"!.
-                       ( $rvalue==$value ? ' SELECTED>' : '>' ).
-                       $record->getfield($def->{select_label}). '</OPTION>';
-            }
+            if ( $def->{select_table} ) {
+              foreach my $record ( qsearch( $def->{select_table}, {} ) ) {
+                my $rvalue = $record->getfield($def->{select_key});
+                $html .= qq!<OPTION VALUE="$rvalue"!.
+                         ( $rvalue==$value ? ' SELECTED>' : '>' ).
+                         $record->getfield($def->{select_label}). '</OPTION>';
+              } #next $record
+            } else { # select_list
+              foreach my $item ( @{$def->{select_list}} ) {
+                $html .= qq!<OPTION VALUE="$item"!.
+                         ( $item eq $value ? ' SELECTED>' : '>' ).
+                         $item. '</OPTION>';
+              } #next $item
+            } #endif
             $html .= '</SELECT>';
           } elsif ( $def->{type} eq 'radius_usergroup_selector' ) {
             $html .= FS::svc_acct::radius_usergroup_selector(
@@ -229,6 +254,11 @@ my %defs = (
           $html .=
             qq!<INPUT TYPE="text" NAME="${layer}__${field}" VALUE="$value">!;
         }
+
+        if($vfields{$layer}->{$field}) {
+          $html .= qq!<BR><INPUT TYPE="radio" NAME="${layer}__${field}_flag" VALUE="X"!.
+          ' CHECKED'x($flag eq 'X'). ">Excluded ";
+        }
         $html .= "</TD></TR>\n";
       }
       $part_svc->svcpart('') if $clone; #undone
diff --git a/httemplate/edit/part_virtual_field.cgi b/httemplate/edit/part_virtual_field.cgi
new file mode 100644 (file)
index 0000000..8bc8438
--- /dev/null
@@ -0,0 +1,92 @@
+<!-- mason kludge -->
+<%
+my ($vfieldpart, $part_virtual_field);
+
+if ( $cgi->param('error') ) {
+  $part_virtual_field = new FS::part_virtual_field ( {
+    map { $_, scalar($cgi->param($_)) } fields('part_virtual_field')});
+  $vfieldpart = $part_virtual_field->vfieldpart;
+} else {
+  my($query) = $cgi->keywords;
+  if ( $query =~ /^(\d+)$/ ) { #editing
+    $vfieldpart=$1;
+    $part_virtual_field=qsearchs('part_virtual_field',
+        {'vfieldpart' => $vfieldpart})
+      or die "Unknown vfieldpart!";
+  
+  } else { #adding
+    $part_virtual_field = new FS::part_virtual_field({});
+  }
+}
+my $action = $part_virtual_field->vfieldpart ? 'Edit' : 'Add';
+
+my $p1 = popurl(1);
+print header("$action Virtual Field Definition", '');
+
+print qq!<FONT SIZE="+1" COLOR="#ff0000">Error: !, $cgi->param('error'),
+      "</FONT>"
+  if $cgi->param('error');
+%>
+<FORM ACTION="<%=$p1%>process/generic.cgi" METHOD="POST">
+
+<INPUT TYPE="hidden" NAME="table" VALUE="part_virtual_field">
+<INPUT TYPE="hidden" NAME="redirect_ok" 
+    VALUE="<%=popurl(2)%>browse/part_virtual_field.cgi">
+<INPUT TYPE="hidden" NAME="vfieldpart" VALUE="<%=
+  $vfieldpart%>">
+Field #<B><%=$vfieldpart or "(NEW)"%></B><BR><BR>
+
+<%=ntable("#cccccc",2)%>
+  <TR>
+    <TD ALIGN="right">Name</TD>
+    <TD><INPUT TYPE="text" NAME="name" MAXLENGTH=15 VALUE="<%=
+    $part_virtual_field->name%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Table</TD>
+    <TD><% if ($action eq 'Add') { %>
+      <SELECT SIZE=1 NAME="dbtable"><%
+        my $dbdef = dbdef;  # ick
+        foreach my $dbtable ($dbdef->tables) {
+          if ($dbtable !~ /^h_/
+          and $dbdef->table($dbtable)->primary_key) { %>
+            <OPTION VALUE="<%=$dbtable%>"><%=$dbtable%></OPTION><%
+          }
+        }
+      %></SELECT><%
+    } else { # Edit
+    %><%=$part_virtual_field->dbtable%>
+    <INPUT TYPE="hidden" NAME="dbtable" VALUE="<%=$part_virtual_field->dbtable%>">
+ <% } %>
+    </TD>
+  <TR>
+    <TD ALIGN="right">Label</TD>
+    <TD><INPUT TYPE="text" NAME="label" MAXLENGTH="20" VALUE="<%=
+    $part_virtual_field->label%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Length</TD>
+    <TD><INPUT TYPE="text" NAME="length" MAXLENGTH=4 VALUE="<%=
+    $part_virtual_field->length%>"></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">Check</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="check_block"><%=
+    $part_virtual_field->check_block%></TEXTAREA></TD>
+  </TR>
+  <TR>
+    <TD ALIGN="right">List source</TD>
+    <TD><TEXTAREA COLS="20" ROWS="4" NAME="list_source"><%=
+    $part_virtual_field->list_source%></TEXTAREA></TD>
+  </TR>
+</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">
+
+</FORM>
+
+<BR><BR>
+<FONT SIZE=-2>If you don't understand what <I>check_block</I> and 
+<I>list_source</I> mean, <B>LEAVE THEM BLANK</B>.  We mean it.</FONT>
+
+
+</BODY>
+</HTML>
index 1b7fc38..7e40c48 100644 (file)
@@ -19,19 +19,16 @@ my $error = '';
 my $routernum  = $cgi->param('routernum');
 my $routername = $cgi->param('routername');
 my $old = qsearchs('router', { routernum => $routernum });
-my @old_rf;
 my @old_psr;
 
 my $new = new FS::router {
-    routernum  => $routernum,
-    routername => $routername,
-    svcnum     => 0
-    };
+  map {
+    ($_, scalar($cgi->param($_)));
+  } fields('router')
+};
 
 if($old) {
-  if($old->routername ne $new->routername) {
-    $error = $new->replace($old);
-  } #else do nothing
+  $error = $new->replace($old);
 } else {
   $error = $new->insert;
   $routernum = $new->routernum;
@@ -49,21 +46,6 @@ if ($old) {
     }
   }
   check($error);
-  @old_rf = $old->router_field;
-  foreach $rf (@old_rf) {
-    if(my $new_val = $cgi->param('rf_'.$rf->routerfieldpart)) {
-      if($new_val ne $rf->value) {
-        my $new_rf = new FS::router_field 
-         { routernum       => $routernum,
-           value           => $new_val,
-           routerfieldpart => $rf->routerfieldpart };
-       $error = $new_rf->replace($rf);
-      } #else do nothing
-    } else {
-      $error = $rf->delete;
-    }
-    check($error);
-  }
 }
 
 foreach($cgi->param) {
@@ -77,23 +59,10 @@ foreach($cgi->param) {
       $error = $new_psr->insert;
     }
     check($error);
-  } elsif($cgi->param($_) ne '' and /^rf_(\d+)$/) {
-    my $part = $1;
-    if(my @x = grep {$_->routerfieldpart == $part} @old_rf) {
-      # already handled all of these
-    } else {
-      my $new_rf = new FS::router_field
-        { routernum       => $routernum,
-         value           => $cgi->param('rf_'.$part),
-         routerfieldpart => $part };
-      $error = $new_rf->insert;
-      check($error);
-    }
   }
 }
 
 
-
 # Yay, everything worked!
 $dbh->commit or die $dbh->errstr;
 print $cgi->redirect(popurl(3). "browse/router.cgi");
index ab8b9f9..4912a3a 100644 (file)
@@ -9,11 +9,10 @@ my $dbh = FS::UID::dbh;
 $cgi->param('svcnum') =~ /^(\d*)$/ or die "Illegal svcnum!";
 my $svcnum = $1;
 
-my $old; my @old_sbf;
+my $old;
 if ( $svcnum ) {
   $old = qsearchs('svc_broadband', { 'svcnum' => $svcnum } )
     or die "fatal: can't find broadband service (svcnum $svcnum)!";
-  @old_sbf = $old->sb_field;
 } else {
   $old = '';
 }
@@ -32,39 +31,6 @@ if ( $svcnum ) {
   $svcnum = $new->svcnum;
 }
 
-unless ($error) {
-  my $sb_field;
-
-  foreach ($cgi->param) {
-    #warn "\$cgi->param $_: " . $cgi->param($_);
-    if(/^sbf_(\d+)/) {
-      my $part = $1;
-      #warn "\$part $part";
-      $sb_field = new FS::sb_field 
-        { svcnum      => $svcnum,
-          value       => $cgi->param($_),
-          sbfieldpart => $part };
-      if (my @x = grep { $_->sbfieldpart eq $part } @old_sbf) {
-      #if (my $old_sb_field = (grep { $_->sbfieldpart eq $part} @old_Sbf)[0]) {
-        #warn "array: " . scalar(@x);
-        if (length($sb_field->value) && ($sb_field->value ne $x[0]->value)) { 
-          #warn "replacing " . $x[0]->value . " with " . $sb_field->value;
-          $error = $sb_field->replace($x[0]);
-          #$error = $sb_field->replace($old_sb_field);
-        } elsif (length($sb_field->value) == 0) { 
-          #warn "delete";
-          $error = $x[0]->delete;
-        }
-      } else {
-        if (length($sb_field->value) > 0) { 
-          #warn "insert";
-          $error = $sb_field->insert;
-        }
-        # else do nothing
-      }
-    }
-  }
-}
 
 if ( $error ) {
   $cgi->param('error', $error);
index b524c64..5c365a2 100755 (executable)
@@ -16,7 +16,6 @@ if ( $cgi->keywords ) {
 
 my $routernum = $router->routernum;
 my $action = $routernum ? 'Edit' : 'Add';
-my $hashref = $router->hashref;
 
 print header("$action Router", menubar(
   'Main Menu' => "$p",
@@ -28,10 +27,13 @@ if($cgi->param('error')) {
 <% } %>
 
 <FORM ACTION="<%=popurl(1)%>process/router.cgi" METHOD=POST>
+  <INPUT TYPE="hidden" NAME="table" VALUE="router">
+  <INPUT TYPE="hidden" NAME="redirect_ok" VALUE="<%=$p3%>/browse/router.cgi">
+  <INPUT TYPE="hidden" NAME="redirect_error" VALUE="<%=$p3%>/edit/router.cgi">
   <INPUT TYPE="hidden" NAME="routernum" VALUE="<%=$routernum%>">
     Router #<%=$routernum or "(NEW)"%>
 
-<BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$hashref->{routername}%>">
+<BR><BR>Name <INPUT TYPE="text" NAME="routername" SIZE=32 VALUE="<%=$router->routername%>">
 
 <BR><BR>
 Custom fields:
@@ -39,35 +41,11 @@ Custom fields:
 <%=table() %>
 
 <%
-# I know, I know.  Massive false laziness with edit/svc_broadband.cgi.  But 
-# Kristian won't let me generalize the custom field mechanism to every table in 
-# the database, so this is what we get.  <snarl>
-# -- MW
-
-my @part_router_field = qsearch('part_router_field', { });
-my %rf = map { $_->part_router_field->name, $_->value } $router->router_field;
-foreach (sort { $a->name cmp $b->name } @part_router_field) {
-  %>
-  <TR>
-    <TD ALIGN="right"><%=$_->name%></TD>
-    <TD><%
-  if(my @opts = $_->list_values) {
-    %>  <SELECT NAME="rf_<%=$_->routerfieldpart%>" SIZE="1">
-          <%
-    foreach $opt (@opts) {
-      %>  <OPTION VALUE="<%=$opt%>"<%=($opt eq $rf{$_->name}) 
-              ? ' SELECTED' : ''%>>
-            <%=$opt%>
-         </OPTION>
-   <% } %>
-       </SELECT>
- <% } else { %>
-        <INPUT NAME="rf_<%=$_->routerfieldpart%>"
-        VALUE="<%=$rf{$_->name}%>"
-        <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
-  <% } %></TD>
-  </TR>
-<% } %>
+foreach my $field ($router->virtual_fields) {
+  print $router->pvf($field)->widget('HTML', 'edit', 
+        $router->getfield($field));
+}
+%>
 </TABLE>
 
 
@@ -81,7 +59,7 @@ foreach my $part_svc ( qsearch('part_svc', { svcdb    => 'svc_broadband',
   <BR>
   <INPUT TYPE="checkbox" NAME="svcpart_<%=$part_svc->svcpart%>"<%=
       qsearchs('part_svc_router', { svcpart   => $part_svc->svcpart, 
-                                    routernum => $routernum } ) ? 'CHECKED' : ''%> VALUE="ON">
+                                    routernum => $routernum } ) ? ' CHECKED' : ''%> VALUE="ON">
   <A HREF="<%=${p}%>edit/part_svc.cgi?<%=$part_svc->svcpart%>">
     <%=$part_svc->svcpart%>: <%=$part_svc->svc%></A>
   <% } %>
index 4420bb6..44606d9 100755 (executable)
@@ -35,8 +35,6 @@ if ( $cgi->param('error') ) {
 
   } else { #adding
 
-    $svc_acct = new FS::svc_acct({}); 
-
     foreach $_ (split(/-/,$query)) {
       $pkgnum=$1 if /^pkgnum(\d+)$/;
       $svcpart=$1 if /^svcpart(\d+)$/;
@@ -44,6 +42,8 @@ if ( $cgi->param('error') ) {
     $part_svc = qsearchs( 'part_svc', { 'svcpart' => $svcpart } );
     die "No part_svc entry for svcpart $svcpart!" unless $part_svc;
 
+    $svc_acct = new FS::svc_acct({svcpart => $svcpart}); 
+
     $svcnum='';
 
     #set gecos
@@ -281,6 +281,14 @@ if ( $part_svc->part_svc_column('usergroup')->columnflag eq "F" ) {
 }
 print '</TD></TR>';
 
+foreach $field ($svc_acct->virtual_fields) {
+  if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+    # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+    print $svc_acct->pvf($field)->widget('HTML', 'edit', 
+        $svc_acct->getfield($field));
+  }
+}
+  
 #submit
 print qq!</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">!; 
 
index ee7f8be..6828791 100644 (file)
@@ -36,8 +36,6 @@ if ( $cgi->param('error') ) {
 
   } else { #adding
 
-    $svc_broadband = new FS::svc_broadband({});
-
     foreach $_ (split(/-/,$query)) { #get & untaint pkgnum & svcpart
       $pkgnum=$1 if /^pkgnum(\d+)$/;
       $svcpart=$1 if /^svcpart(\d+)$/;
@@ -45,7 +43,7 @@ if ( $cgi->param('error') ) {
     $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
     die "No part_svc entry!" unless $part_svc;
 
-    $svc_broadband->setfield('svcpart', $svcpart);
+    $svc_broadband = new FS::svc_broadband({ svcpart => $svcpart });
 
     $svcnum='';
 
@@ -159,30 +157,12 @@ Service #<B><%=$svcnum ? $svcnum : "(NEW)"%></B><BR><BR>
 <% } %>
 
 <%
-
-  my @part_sb_field = qsearch('part_sb_field', { svcpart => $svcpart });
-  my $sbf_hashref = $svc_broadband->sb_field_hashref($svcpart);
-  foreach (sort { $a->name cmp $b->name } @part_sb_field) {
-    %>
-    <TR>
-      <TD ALIGN="right"><%=$_->name%></TD>
-      <TD><%
-      if(my @opts = $_->list_values) {
-        %>
-       <SELECT NAME="sbf_<%=$_->sbfieldpart%>" SIZE=1> <%
-        foreach $opt (@opts) { %>
-          <OPTION VALUE="<%=$opt%>"<%=
-           ($opt eq $sbf_hashref->{$_->name}) ? ' SELECTED' : ''%>>
-           <%=$opt%></OPTION><%
-        } %></SELECT>
-   <% } else { %>
-        <INPUT NAME="sbf_<%=$_->sbfieldpart%>"
-           VALUE="<%=$sbf_hashref->{$_->name}%>"
-     <%=$_->length ? 'SIZE="'.$_->length.'"' : ''%>>
-   <% } %>
-      </TD>
-    </TR>
-<% } %>
+foreach $field ($svc_broadband->virtual_fields) {
+  if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+    print $svc_broadband->pvf($field)->widget('HTML', 'edit',
+        $svc_broadband->getfield($field));
+  }
+} %>
   </TABLE>
   <BR>
   <INPUT TYPE="submit" NAME="submit" VALUE="Submit">
index d2c9ade..043af61 100644 (file)
@@ -29,12 +29,12 @@ if ( $cgi->param('error') ) {
 
   } else { #adding
 
-    $svc_www = new FS::svc_www({});
-
     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 };
+
     $part_svc=qsearchs('part_svc',{'svcpart'=>$svcpart});
     die "No part_svc entry!" unless $part_svc;
 
@@ -167,6 +167,14 @@ foreach $_ (keys %username) {
 }
 print "</SELECT></TD></TR>";
 
+foreach $field ($svc_www->virtual_fields) {
+  if ( $part_svc->part_svc_column($field)->columnflag ne 'F' ) {
+    # If the flag is X, it won't even show up in $svc_acct->virtual_fields.
+    print $svc_www->pvf($field)->widget('HTML', 'edit', 
+        $svc_www->getfield($field));
+  }
+}
+
 print '</TABLE><BR><INPUT TYPE="submit" VALUE="Submit">';
 
 print <<END;
index 017ffcd..1895f29 100644 (file)
@@ -11,7 +11,7 @@
     </td><td>
       <font color="#ff0000" size=7>freeside main menu</font>
     </td><td align=right valign=bottom>
-      version %%%VERSION%%%
+      version 1.5.0pre2
       <BR><A HREF="http://www.sisd.com/freeside">Freeside home page</A>
       <BR><A HREF="docs/">Documentation</A>
     </td></tr>
             - Points of Presence 
           <LI><A HREF="browse/part_bill_event.cgi">View/Edit invoice events</A> - Actions for overdue invoices
          <LI><A HREF="browse/msgcat.cgi">View/Edit message catalog</A> - Change error messages and other customizable labels.
-         <LI><A HREF="browse/part_sb_field.cgi">View/Edit custom svc_broadband fields</A>
-         - Custom broadband service fields for site-specific export/informational data.
-         <LI><A HREF="browse/generic.cgi?part_router_field">View/Edit custom router fields</A>
-         - Custom router fields for site-specific export data.
+    <LI><A HREF="browse/part_virtual_field.cgi">View/Edit virtual fields</A>
+         - Locally defined fields
          <LI><A HREF="browse/router.cgi">View/Edit routers</A>
          - Broadband access routers
          <LI><A HREF="browse/addr_block.cgi">View/Edit address blocks</A>
index 599c1d8..4e01db3 100755 (executable)
@@ -172,7 +172,7 @@ if ($svc_acct->slipip) {
           : $svc_acct->slipip
         ). "</TD>";
   my($attribute);
-  foreach $attribute ( grep /^radius_/, fields('svc_acct') ) {
+  foreach $attribute ( grep /^radius_/, $svc_acct->fields ) {
     #warn $attribute;
     $attribute =~ /^radius_(.*)$/;
     my $pattribute = $FS::raddb::attrib{$1};
@@ -180,7 +180,7 @@ if ($svc_acct->slipip) {
           "<TD BGCOLOR=\"#ffffff\">". $svc_acct->getfield($attribute).
           "</TD></TR>";
   }
-  foreach $attribute ( grep /^rc_/, fields('svc_acct') ) {
+  foreach $attribute ( grep /^rc_/, $svc_acct->fields ) {
     #warn $attribute;
     $attribute =~ /^rc_(.*)$/;
     my $pattribute = $FS::raddb::attrib{$1};
@@ -195,7 +195,19 @@ if ($svc_acct->slipip) {
 print '<TR><TD ALIGN="right">RADIUS groups</TD><TD BGCOLOR="#ffffff">'.
       join('<BR>', $svc_acct->radius_groups). '</TD></TR>';
 
-print '</TABLE></TD></TR></TABLE><BR><BR>';
+# Can this be abstracted further?  Maybe a library function like
+# widget('HTML', 'view', $svc_acct) ?  It would definitely make UI 
+# style management easier.
+
+foreach (sort { $a cmp $b } $svc_acct->virtual_fields) {
+  print $svc_acct->pvf($_)->widget('HTML', 'view', $svc_acct->getfield($_)),
+      "\n";
+}
+%>
+</TABLE></TD></TR></TABLE>
+<%
+
+print '<BR><BR>';
 
 print join("\n", $conf->config('svc_acct-notes') ). '<BR><BR>'.
       joblisting({'svcnum'=>$svcnum}, 1). '</BODY></HTML>';
index 164b5b2..677a4b0 100644 (file)
@@ -64,26 +64,12 @@ print header('Broadband Service View', menubar(
         qq!<TD BGCOLOR="#ffffff">$ip_addr</TD></TR>!.
       '</TD></TR><TR ROWSPAN="1"><TD></TD></TR>';
 
+foreach (sort { $a cmp $b } $svc_broadband->virtual_fields) {
+  print $svc_broadband->pvf($_)->widget('HTML', 'view', 
+      $svc_broadband->getfield($_)), "\n";
+}
 
-#  foreach my $sb_field 
-#      ( qsearch('sb_field', { svcnum => $svcnum }) ) {
-#    my $part_sb_field = qsearchs('part_sb_field',
-#                         { sbfieldpart => $sb_field->sbfieldpart });
-#    print q!<TR><TD ALIGN="right">! . $part_sb_field->name . 
-#          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->value . 
-#          q!</TD></TR>!;
-#  }
-#  print '</TABLE>';
-
-
-  my $sb_field = $svc_broadband->sb_field_hashref;
-  foreach (sort { $a cmp $b } keys(%{$sb_field})) {
-    print q!<TR><TD ALIGN="right">! . $_ . 
-          q!</TD><TD BGCOLOR="#ffffff">! . $sb_field->{$_} .
-          q!</TD></TR>!;
-  }
-  print '</TABLE>';
-
+print '</TABLE>';
 
 print '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
       '</BODY></HTML>'
index c8d1d62..5d619de 100755 (executable)
@@ -61,7 +61,14 @@ print qq!<A HREF="${p}edit/svc_forward.cgi?$svcnum">Edit this information</A>!.
       qq!<TR><TD ALIGN="right">Email to</TD>!.
         qq!<TD BGCOLOR="#ffffff">$source</TD></TR>!.
       qq!<TR><TD ALIGN="right">Forwards to </TD>!.
-        qq!<TD BGCOLOR="#ffffff">$destination</TD></TR></TABLE>!.
+        qq!<TD BGCOLOR="#ffffff">$destination</TD></TR>!;
+
+foreach (sort { $a cmp $b } $svc_forward->virtual_fields) {
+  print $svc_forward->pvf($_)->widget('HTML', 'view', $svc_forward->getfield($_)),
+      "\n";
+}
+
+print qq!  </TABLE>!.
       '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
       '</BODY></HTML>'
 ;
index 4426144..7a716b4 100644 (file)
@@ -47,8 +47,15 @@ print header('Website View', menubar(
       qq!<TR><TD ALIGN="right">Website name</TD>!.
         qq!<TD BGCOLOR="#ffffff"><A HREF="http://$www">$www<A></TD></TR>!.
       qq!<TR><TD ALIGN="right">Account</TD>!.
-        qq!<TD BGCOLOR="#ffffff"><A HREF="${p}view/svc_acct.cgi?$usersvc">$email</A></TD></TR>!.
-      '</TABLE></TD></TR></TABLE>'.
+        qq!<TD BGCOLOR="#ffffff"><A HREF="${p}view/svc_acct.cgi?$usersvc">$email</A></TD></TR>!;
+
+foreach (sort { $a cmp $b } $svc_www->virtual_fields) {
+  print $svc_www->pvf($_)->widget('HTML', 'view', $svc_www->getfield($_)),
+      "\n";
+}
+
+
+print '</TABLE></TD></TR></TABLE>'.
       '<BR>'. joblisting({'svcnum'=>$svcnum}, 1).
       '</BODY></HTML>'
 ;