initial h2xs
[freeside.git] / site_perl / cust_main.pm
index 83a7d78..6140dcc 100644 (file)
@@ -5,29 +5,37 @@ use vars qw($paymentserversecret $paymentserverport $paymentserverhost);
 package FS::cust_main;
 
 use strict;
 package FS::cust_main;
 
 use strict;
-use vars qw(@ISA @EXPORT_OK $conf $lpr $processor $xaction $E_NoErr);
+use vars qw( @ISA $conf $lpr $processor $xaction $E_NoErr $invoice_from
+             $smtpmachine );
 use Safe;
 use Safe;
-use Exporter;
 use Carp;
 use Time::Local;
 use Date::Format;
 use Date::Manip;
 use Carp;
 use Time::Local;
 use Date::Format;
 use Date::Manip;
+use Mail::Internet;
+use Mail::Header;
 use Business::CreditCard;
 use Business::CreditCard;
-use FS::UID qw(getotaker);
-use FS::Record qw(fields hfields qsearchs qsearch);
+use FS::UID qw( getotaker );
+use FS::Record qw( qsearchs qsearch );
 use FS::cust_pkg;
 use FS::cust_bill;
 use FS::cust_bill_pkg;
 use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_bill;
 use FS::cust_bill_pkg;
 use FS::cust_pay;
-#use FS::cust_pay_batch;
+use FS::cust_credit;
+use FS::cust_pay_batch;
+use FS::part_referral;
+use FS::cust_main_county;
+use FS::agent;
+use FS::cust_main_invoice;
 
 
-@ISA = qw(FS::Record Exporter);
-@EXPORT_OK = qw(hfields);
+@ISA = qw( FS::Record );
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::cust_main'} = sub { 
   $conf = new FS::Conf;
   $lpr = $conf->config('lpr');
 
 #ask FS::UID to run this stuff for us later
 $FS::UID::callback{'FS::cust_main'} = sub { 
   $conf = new FS::Conf;
   $lpr = $conf->config('lpr');
+  $invoice_from = $conf->config('invoice_from');
+  $smtpmachine = $conf->config('smtpmachine');
 
   if ( $conf->exists('cybercash3.2') ) {
     require CCMckLib3_2;
 
   if ( $conf->exists('cybercash3.2') ) {
     require CCMckLib3_2;
@@ -70,8 +78,8 @@ FS::cust_main - Object methods for cust_main records
 
   use FS::cust_main;
 
 
   use FS::cust_main;
 
-  $record = create FS::cust_main \%hash;
-  $record = create FS::cust_main { 'column' => 'value' };
+  $record = new FS::cust_main \%hash;
+  $record = new FS::cust_main { 'column' => 'value' };
 
   $error = $record->insert;
 
 
   $error = $record->insert;
 
@@ -135,6 +143,8 @@ FS::Record.  The following fields are currently supported:
 
 =item night - phone (optional)
 
 
 =item night - phone (optional)
 
+=item fax - phone (optional)
+
 =item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
 
 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
 =item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
 
 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
@@ -153,7 +163,7 @@ FS::Record.  The following fields are currently supported:
 
 =over 4
 
 
 =over 4
 
-=item create HASHREF
+=item new HASHREF
 
 Creates a new customer.  To add the customer to the database, see L<"insert">.
 
 
 Creates a new customer.  To add the customer to the database, see L<"insert">.
 
@@ -162,55 +172,68 @@ points to.  You can ask the object for a copy with the I<hash> method.
 
 =cut
 
 
 =cut
 
-sub create {
-  my($proto,$hashref)=@_;
-
-  #now in FS::Record::new
-  #my $field;
-  #foreach $field (fields('cust_main')) {
-  #  $hashref->{$field}='' unless defined $hashref->{$field};
-  #}
-
-  $proto->new('cust_main',$hashref);
-}
+sub table { 'cust_main'; }
 
 =item insert
 
 Adds this customer to the database.  If there is an error, returns the error,
 otherwise returns false.
 
 
 =item insert
 
 Adds this customer to the database.  If there is an error, returns the error,
 otherwise returns false.
 
-=cut
-
-sub insert {
-  my($self)=@_;
-
-  #no callbacks in check, only data checks
-  #local $SIG{HUP} = 'IGNORE';
-  #local $SIG{INT} = 'IGNORE';
-  #local $SIG{QUIT} = 'IGNORE';
-  #local $SIG{TERM} = 'IGNORE';
-  #local $SIG{TSTP} = 'IGNORE';
+=item delete NEW_CUSTNUM
 
 
-  $self->check or
-  $self->add;
-}
+This deletes the customer.  If there is an error, returns the error, otherwise
+returns false.
 
 
-=item delete
+This will completely remove all traces of the customer record.  This is not
+what you want when a customer cancels service; for that, cancel all of the
+customer's packages (see L<FS::cust_pkg/cancel>).
 
 
-Currently unimplemented.  Maybe cancel all of this customer's
-packages (cust_pkg)?
+If the customer has any packages, you need to pass a new (valid) customer
+number for those packages to be transferred to.
 
 
-I don't remove the customer record in the database because there would then
-be no record the customer ever existed (which is bad, no?)
+You can't delete a customer with invoices (see L<FS::cust_bill>),
+or credits (see L<FS::cust_credit>).
 
 =cut
 
 
 =cut
 
-# Usage: $error = $record -> delete;
 sub delete {
 sub delete {
-   return "Can't (yet?) delete customers.";
-#  my($self)=@_;
-#
-#  $self->del;
+  my $self = shift;
+
+  if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
+    return "Can't delete a customer with invoices";
+  }
+  if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
+    return "Can't delete a customer with credits";
+  }
+
+  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 @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
+  if ( @cust_pkg ) {
+    my $new_custnum = shift;
+    return "Invalid new customer number: $new_custnum"
+      unless qsearchs( 'cust_main', { 'custnum' => $new_custnum } );
+    foreach my $cust_pkg ( @cust_pkg ) {
+      my %hash = $cust_pkg->hash;
+      $hash{'custnum'} = $new_custnum;
+      my $new_cust_pkg = new FS::cust_pkg ( \%hash );
+      my $error = $new_cust_pkg->replace($cust_pkg);
+      return $error if $error;
+    }
+  }
+  foreach my $cust_main_invoice (
+    qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
+  ) {
+    my $error = $cust_main_invoice->delete;
+    return $error if $error;
+  }
+
+  $self->SUPER::delete;
 }
 
 =item replace OLD_RECORD
 }
 
 =item replace OLD_RECORD
@@ -218,17 +241,6 @@ sub delete {
 Replaces the OLD_RECORD with this one in the database.  If there is an error,
 returns the error, otherwise returns false.
 
 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)=@_;
-  return "(Old) Not a cust_main record!" unless $old->table eq "cust_main";
-  return "Can't change custnum!"
-    unless $old->getfield('custnum') eq $new->getfield('custnum');
-  $new->check or
-  $new->rep($old);
-}
-
 =item check
 
 Checks all fields to make sure this is a valid customer record.  If there is
 =item check
 
 Checks all fields to make sure this is a valid customer record.  If there is
@@ -238,19 +250,18 @@ and repalce methods.
 =cut
 
 sub check {
 =cut
 
 sub check {
-  my($self)=@_;
-
-  return "Not a cust_main record!" unless $self->table eq "cust_main";
+  my $self = shift;
 
   my $error =
 
   my $error =
-    $self->ut_number('agentnum')
+    $self->ut_numbern('custnum')
+    || $self->ut_number('agentnum')
     || $self->ut_number('refnum')
     || $self->ut_textn('company')
     || $self->ut_text('address1')
     || $self->ut_textn('address2')
     || $self->ut_text('city')
     || $self->ut_textn('county')
     || $self->ut_number('refnum')
     || $self->ut_textn('company')
     || $self->ut_text('address1')
     || $self->ut_textn('address2')
     || $self->ut_text('city')
     || $self->ut_textn('county')
-    || $self->ut_text('state')
+    || $self->ut_textn('state')
     || $self->ut_phonen('daytime')
     || $self->ut_phonen('night')
     || $self->ut_phonen('fax')
     || $self->ut_phonen('daytime')
     || $self->ut_phonen('night')
     || $self->ut_phonen('fax')
@@ -258,15 +269,17 @@ sub check {
   return $error if $error;
 
   return "Unknown agent"
   return $error if $error;
 
   return "Unknown agent"
-    unless qsearchs('agent',{'agentnum'=>$self->agentnum});
+    unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
 
   return "Unknown referral"
 
   return "Unknown referral"
-    unless qsearchs('part_referral',{'refnum'=>$self->refnum});
+    unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
 
 
-  $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
+  $self->getfield('last') =~ /^([\w \,\.\-\']+)$/
+    or return "Illegal last name: ". $self->getfield('last');
   $self->setfield('last',$1);
 
   $self->setfield('last',$1);
 
-  $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
+  $self->first =~ /^([\w \,\.\-\']+)$/
+    or return "Illegal first name: ". $self->first;
   $self->first($1);
 
   if ( $self->ss eq '' ) {
   $self->first($1);
 
   if ( $self->ss eq '' ) {
@@ -275,25 +288,31 @@ sub check {
     my $ss = $self->ss;
     $ss =~ s/\D//g;
     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
     my $ss = $self->ss;
     $ss =~ s/\D//g;
     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
-      or return "Illegal social security number";
+      or return "Illegal social security number: ". $self->ss;
     $self->ss("$1-$2-$3");
   }
 
     $self->ss("$1-$2-$3");
   }
 
-  return "Unknown state/county/country"
-    unless qsearchs('cust_main_county',{
-      'state'  => $self->state,
-      'county' => $self->county,
-    } );
+  $self->country =~ /^(\w\w)$/ or return "Illegal country: ". $self->country;
+  $self->country($1);
+  unless ( qsearchs('cust_main_county', {
+    'country' => $self->country,
+    'state'   => '',
+   } ) ) {
+    return "Unknown state/county/country: ".
+      $self->state. "/". $self->county. "/". $self->country
+      unless qsearchs('cust_main_county',{
+        'state'   => $self->state,
+        'county'  => $self->county,
+        'country' => $self->country,
+      } );
+  }
 
 
-  #int'l zips?
-  $self->zip =~ /^(\d{5}(-\d{4})?)$/ or return "Illegal zip";
+  $self->zip =~ /^\s*(\w[\w\-\s]{3,8}\w)\s*$/
+    or return "Illegal zip: ". $self->zip;
   $self->zip($1);
 
   $self->zip($1);
 
-  #int'l countries!
-  $self->country =~ /^(US)$/ or return "Illegal country";
-  $self->country($1);
-
-  $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
+  $self->payby =~ /^(CARD|BILL|COMP)$/
+    or return "Illegal payby: ". $self->payby;
   $self->payby($1);
 
   if ( $self->payby eq 'CARD' ) {
   $self->payby($1);
 
   if ( $self->payby eq 'CARD' ) {
@@ -301,26 +320,22 @@ sub check {
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
     $payinfo =~ /^(\d{13,16})$/
     my $payinfo = $self->payinfo;
     $payinfo =~ s/\D//g;
     $payinfo =~ /^(\d{13,16})$/
-      or return "Illegal credit card number";
+      or return "Illegal credit card number: ". $self->payinfo;
     $payinfo = $1;
     $self->payinfo($payinfo);
     $payinfo = $1;
     $self->payinfo($payinfo);
-    validate($payinfo) or return "Illegal credit card number";
-    my $type = cardtype($payinfo);
-    return "Unknown credit card type"
-      unless ( $type =~ /^VISA/ ||
-               $type =~ /^MasterCard/ ||
-               $type =~ /^American Express/ ||
-               $type =~ /^Discover/ );
+    validate($payinfo)
+      or return "Illegal credit card number: ". $self->payinfo;
+    return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
 
   } elsif ( $self->payby eq 'BILL' ) {
 
 
   } elsif ( $self->payby eq 'BILL' ) {
 
-    $self->payinfo =~ /^([\w \-]*)$/ or return "Illegal P.O. number";
-    $self->payinfo($1);
+    $error = $self->ut_textn('payinfo');
+    return "Illegal P.O. number: ". $self->payinfo if $error;
 
   } elsif ( $self->payby eq 'COMP' ) {
 
 
   } elsif ( $self->payby eq 'COMP' ) {
 
-    $self->payinfo =~ /^(\w{2,8})$/ or return "Illegal comp account issuer";
-    $self->payinfo($1);
+    $error = $self->ut_textn('payinfo');
+    return "Illegal comp account issuer: ". $self->payinfo if $error;
 
   }
 
 
   }
 
@@ -329,7 +344,7 @@ sub check {
     $self->paydate('');
   } else {
     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
     $self->paydate('');
   } else {
     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
-      or return "Illegal expiration date";
+      or return "Illegal expiration date: ". $self->paydate;
     if ( length($2) == 4 ) {
       $self->paydate("$2-$1-01");
     } elsif ( $2 > 97 ) { #should pry change to check for "this year"
     if ( length($2) == 4 ) {
       $self->paydate("$2-$1-01");
     } elsif ( $2 > 97 ) { #should pry change to check for "this year"
@@ -343,11 +358,11 @@ sub check {
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
     $self->payname =~ /^([\w \,\.\-\']+)$/
     $self->payname( $self->first. " ". $self->getfield('last') );
   } else {
     $self->payname =~ /^([\w \,\.\-\']+)$/
-      or return "Illegal billing name";
+      or return "Illegal billing name: ". $self->payname;
     $self->payname($1);
   }
 
     $self->payname($1);
   }
 
-  $self->tax =~ /^(Y?)$/ or return "Illegal tax";
+  $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
   $self->tax($1);
 
   $self->otaker(getotaker);
   $self->tax($1);
 
   $self->otaker(getotaker);
@@ -362,7 +377,7 @@ Returns all packages (see L<FS::cust_pkg>) for this customer.
 =cut
 
 sub all_pkgs {
 =cut
 
 sub all_pkgs {
-  my($self)=@_;
+  my $self = shift;
   qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
 }
 
   qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
 }
 
@@ -373,7 +388,7 @@ Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
 =cut
 
 sub ncancelled_pkgs {
 =cut
 
 sub ncancelled_pkgs {
-  my($self)=@_;
+  my $self = shift;
   qsearch( 'cust_pkg', {
     'custnum' => $self->custnum,
     'cancel'  => '',
   qsearch( 'cust_pkg', {
     'custnum' => $self->custnum,
     'cancel'  => '',
@@ -395,10 +410,10 @@ If there is an error, returns the error, otherwise returns false.
 =cut
 
 sub bill {
 =cut
 
 sub bill {
-  my($self,%options)=@_;
-  my($time) = $options{'time'} || $^T;
+  my( $self, %options ) = @_;
+  my $time = $options{'time'} || time;
 
 
-  my($error);
+  my $error;
 
   #put below somehow?
   local $SIG{HUP} = 'IGNORE';
 
   #put below somehow?
   local $SIG{HUP} = 'IGNORE';
@@ -406,42 +421,38 @@ sub bill {
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
   local $SIG{TSTP} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
   local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
 
   # find the packages which are due for billing, find out how much they are
   # & generate invoice database.
  
 
   # find the packages which are due for billing, find out how much they are
   # & generate invoice database.
  
-  my($total_setup,$total_recur)=(0,0);
-
-  my(@cust_bill_pkg);
+  my( $total_setup, $total_recur ) = ( 0, 0 );
+  my @cust_bill_pkg;
 
 
-  my($cust_pkg);
-  foreach $cust_pkg (
+  foreach my $cust_pkg (
     qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
   ) {
 
     qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
   ) {
 
-    bless($cust_pkg,"FS::cust_pkg");
-    next if ( $cust_pkg->getfield('cancel') );  
+    next if $cust_pkg->getfield('cancel');  
 
     #? to avoid use of uninitialized value errors... ?
     $cust_pkg->setfield('bill', '')
       unless defined($cust_pkg->bill);
  
 
     #? to avoid use of uninitialized value errors... ?
     $cust_pkg->setfield('bill', '')
       unless defined($cust_pkg->bill);
  
-    my($part_pkg)=
-      qsearchs('part_pkg',{'pkgpart'=> $cust_pkg->pkgpart } );
+    my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
 
     #so we don't modify cust_pkg record unnecessarily
 
     #so we don't modify cust_pkg record unnecessarily
-    my($cust_pkg_mod_flag)=0;
-    my(%hash)=$cust_pkg->hash;
-    my($old_cust_pkg)=create FS::cust_pkg(\%hash);
+    my $cust_pkg_mod_flag = 0;
+    my %hash = $cust_pkg->hash;
+    my $old_cust_pkg = new FS::cust_pkg \%hash;
 
     # bill setup
 
     # bill setup
-    my($setup)=0;
+    my $setup = 0;
     unless ( $cust_pkg->setup ) {
     unless ( $cust_pkg->setup ) {
-      my($setup_prog)=$part_pkg->getfield('setup');
-      my($cpt) = new Safe;
+      my $setup_prog = $part_pkg->getfield('setup');
+      my $cpt = new Safe;
       #$cpt->permit(); #what is necessary?
       #$cpt->permit(); #what is necessary?
-      $cpt->share(qw($cust_pkg)); #can $cpt now use $cust_pkg methods?
+      $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
       $setup = $cpt->reval($setup_prog);
       unless ( defined($setup) ) {
         warn "Error reval-ing part_pkg->setup pkgpart ", 
       $setup = $cpt->reval($setup_prog);
       unless ( defined($setup) ) {
         warn "Error reval-ing part_pkg->setup pkgpart ", 
@@ -453,16 +464,16 @@ sub bill {
     }
 
     #bill recurring fee
     }
 
     #bill recurring fee
-    my($recur)=0;
-    my($sdate);
+    my $recur = 0;
+    my $sdate;
     if ( $part_pkg->getfield('freq') > 0 &&
          ! $cust_pkg->getfield('susp') &&
          ( $cust_pkg->getfield('bill') || 0 ) < $time
     ) {
     if ( $part_pkg->getfield('freq') > 0 &&
          ! $cust_pkg->getfield('susp') &&
          ( $cust_pkg->getfield('bill') || 0 ) < $time
     ) {
-      my($recur_prog)=$part_pkg->getfield('recur');
-      my($cpt) = new Safe;
+      my $recur_prog = $part_pkg->getfield('recur');
+      my $cpt = new Safe;
       #$cpt->permit(); #what is necessary?
       #$cpt->permit(); #what is necessary?
-      $cpt->share(qw($cust_pkg)); #can $cpt now use $cust_pkg methods?
+      $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
       $recur = $cpt->reval($recur_prog);
       unless ( defined($recur) ) {
         warn "Error reval-ing part_pkg->recur pkgpart ",
       $recur = $cpt->reval($recur_prog);
       unless ( defined($recur) ) {
         warn "Error reval-ing part_pkg->recur pkgpart ",
@@ -471,13 +482,14 @@ sub bill {
         #change this bit to use Date::Manip?
         #$sdate=$cust_pkg->bill || time;
         #$sdate=$cust_pkg->bill || $time;
         #change this bit to use Date::Manip?
         #$sdate=$cust_pkg->bill || time;
         #$sdate=$cust_pkg->bill || $time;
-        $sdate=$cust_pkg->bill || $cust_pkg->setup || $time;
-        my($sec,$min,$hour,$mday,$mon,$year)=
+        $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
+        my ($sec,$min,$hour,$mday,$mon,$year) =
           (localtime($sdate) )[0,1,2,3,4,5];
         $mon += $part_pkg->getfield('freq');
         until ( $mon < 12 ) { $mon -= 12; $year++; }
           (localtime($sdate) )[0,1,2,3,4,5];
         $mon += $part_pkg->getfield('freq');
         until ( $mon < 12 ) { $mon -= 12; $year++; }
-        $cust_pkg->setfield('bill',timelocal($sec,$min,$hour,$mday,$mon,$year));
-        $cust_pkg_mod_flag=1; 
+        $cust_pkg->setfield('bill',
+          timelocal($sec,$min,$hour,$mday,$mon,$year));
+        $cust_pkg_mod_flag = 1; 
       }
     }
 
       }
     }
 
@@ -485,15 +497,14 @@ sub bill {
     warn "recur is undefinded" unless defined($recur);
     warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
 
     warn "recur is undefinded" unless defined($recur);
     warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
 
-    if ($cust_pkg_mod_flag) {
+    if ( $cust_pkg_mod_flag ) {
       $error=$cust_pkg->replace($old_cust_pkg);
       $error=$cust_pkg->replace($old_cust_pkg);
-      if ( $error ) {
+      if ( $error ) { #just in case
         warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
       } else {
         warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
       } else {
-        #just in case
-        $setup=sprintf("%.2f",$setup);
-        $recur=sprintf("%.2f",$recur);
-        my($cust_bill_pkg)=create FS::cust_bill_pkg ({
+        $setup = sprintf( "%.2f", $setup );
+        $recur = sprintf( "%.2f", $recur );
+        my $cust_bill_pkg = new FS::cust_bill_pkg ({
           'pkgnum' => $cust_pkg->pkgnum,
           'setup'  => $setup,
           'recur'  => $recur,
           'pkgnum' => $cust_pkg->pkgnum,
           'setup'  => $setup,
           'recur'  => $recur,
@@ -508,24 +519,24 @@ sub bill {
 
   }
 
 
   }
 
-  my($charged)=sprintf("%.2f",$total_setup + $total_recur);
+  my $charged = sprintf( "%.2f", $total_setup + $total_recur );
 
   return '' if scalar(@cust_bill_pkg) == 0;
 
 
   return '' if scalar(@cust_bill_pkg) == 0;
 
-  unless ( $self->getfield('tax') eq 'Y' ||
-           $self->getfield('tax') eq 'y' ||
-           $self->getfield('payby') eq 'COMP'
+  unless ( $self->getfield('tax') =~ /Y/i
+           || $self->getfield('payby') eq 'COMP'
   ) {
   ) {
-    my($cust_main_county) = qsearchs('cust_main_county',{
-      'county' => $self->getfield('county'),
-      'state'  => $self->getfield('state'),
+    my $cust_main_county = qsearchs('cust_main_county',{
+        'state'   => $self->state,
+        'county'  => $self->county,
+        'country' => $self->country,
     } );
     } );
-    my($tax) = sprintf("%.2f",
+    my $tax = sprintf( "%.2f",
       $charged * ( $cust_main_county->getfield('tax') / 100 )
     );
       $charged * ( $cust_main_county->getfield('tax') / 100 )
     );
-    $charged = sprintf("%.2f",$charged+$tax);
+    $charged = sprintf( "%.2f", $charged+$tax );
 
 
-    my($cust_bill_pkg)=create FS::cust_bill_pkg ({
+    my $cust_bill_pkg = new FS::cust_bill_pkg ({
       'pkgnum' => 0,
       'setup'  => $tax,
       'recur'  => 0,
       'pkgnum' => 0,
       'setup'  => $tax,
       'recur'  => 0,
@@ -535,23 +546,23 @@ sub bill {
     push @cust_bill_pkg, $cust_bill_pkg;
   }
 
     push @cust_bill_pkg, $cust_bill_pkg;
   }
 
-  my($cust_bill) = create FS::cust_bill ( {
+  my $cust_bill = new FS::cust_bill ( {
     'custnum' => $self->getfield('custnum'),
     '_date' => $time,
     'charged' => $charged,
   } );
     'custnum' => $self->getfield('custnum'),
     '_date' => $time,
     'charged' => $charged,
   } );
-  $error=$cust_bill->insert;
+  $error = $cust_bill->insert;
   #shouldn't happen, but how else to handle this? (wrap me in eval, to catch 
   # fatal errors)
   die "Error creating cust_bill record: $error!\n",
       "Check updated but unbilled packages for customer", $self->custnum, "\n"
     if $error;
 
   #shouldn't happen, but how else to handle this? (wrap me in eval, to catch 
   # fatal errors)
   die "Error creating cust_bill record: $error!\n",
       "Check updated but unbilled packages for customer", $self->custnum, "\n"
     if $error;
 
-  my($invnum)=$cust_bill->invnum;
-  my($cust_bill_pkg);
+  my $invnum = $cust_bill->invnum;
+  my $cust_bill_pkg;
   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
-    $cust_bill_pkg->setfield('invnum',$invnum);
-    $error=$cust_bill_pkg->insert;
+    $cust_bill_pkg->setfield( 'invnum', $invnum );
+    $error = $cust_bill_pkg->insert;
     #shouldn't happen, but how else tohandle this?
     die "Error creating cust_bill_pkg record: $error!\n",
         "Check incomplete invoice ", $invnum, "\n"
     #shouldn't happen, but how else tohandle this?
     die "Error creating cust_bill_pkg record: $error!\n",
         "Check incomplete invoice ", $invnum, "\n"
@@ -587,10 +598,10 @@ return an error.  By default, they don't.
 =cut
 
 sub collect {
 =cut
 
 sub collect {
-  my($self,%options)=@_;
-  my($invoice_time) = $options{'invoice_time'} || $^T;
+  my( $self, %options ) = @_;
+  my $invoice_time = $options{'invoice_time'} || time;
 
 
-  my($total_owed) = $self->balance;
+  my $total_owed = $self->balance;
   return '' unless $total_owed > 0; #redundant?????
 
   #put below somehow?
   return '' unless $total_owed > 0; #redundant?????
 
   #put below somehow?
@@ -599,91 +610,109 @@ sub collect {
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
   local $SIG{TSTP} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
   local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
 
 
-  foreach my $cust_bill ( qsearch('cust_bill', {
-    'custnum' => $self->getfield('custnum'),
-  } ) ) {
-
-    bless($cust_bill,"FS::cust_bill");
+  foreach my $cust_bill (
+    qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+  ) {
 
     #this has to be before next's
 
     #this has to be before next's
-    my($amount) = sprintf("%.2f", $total_owed < $cust_bill->owed
+    my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
                                   ? $total_owed
                                   : $cust_bill->owed
     );
                                   ? $total_owed
                                   : $cust_bill->owed
     );
-    $total_owed = sprintf("%.2f",$total_owed-$amount);
+    $total_owed = sprintf( "%.2f", $total_owed - $amount );
 
     next unless $cust_bill->owed > 0;
 
 
     next unless $cust_bill->owed > 0;
 
-    next if qsearchs('cust_pay_batch',{'invnum'=> $cust_bill->invnum });
+    next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
 
     #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
 
     next unless $amount > 0;
 
 
     #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
 
     next unless $amount > 0;
 
-    if ( $self->getfield('payby') eq 'BILL' ) {
+    if ( $self->payby eq 'BILL' ) {
 
       #30 days 2592000
 
       #30 days 2592000
-      my($since)=$invoice_time - ( $cust_bill->_date || 0 );
+      my $since = $invoice_time - ( $cust_bill->_date || 0 );
       #warn "$invoice_time ", $cust_bill->_date, " $since";
       if ( $since >= 0 #don't print future invoices
            && ( $cust_bill->printed * 2592000 ) <= $since
       ) {
 
       #warn "$invoice_time ", $cust_bill->_date, " $since";
       if ( $since >= 0 #don't print future invoices
            && ( $cust_bill->printed * 2592000 ) <= $since
       ) {
 
-        open(LPR,"|$lpr") or die "Can't open $lpr: $!";
-        print LPR $cust_bill->print_text; #( date )
-        close LPR
-          or die $! ? "Error closing $lpr: $!"
-                       : "Exit status $? from $lpr";
+        #my @print_text = $cust_bill->print_text; #( date )
+        my @invoicing_list = $self->invoicing_list;
+        if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
+          $ENV{SMTPHOSTS} = $smtpmachine;
+          $ENV{MAILADDRESS} = $invoice_from;
+          my $header = new Mail::Header ( [
+            "From: $invoice_from",
+            "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
+            "Sender: $invoice_from",
+            "Reply-To: $invoice_from",
+            "Date: ". time2str("%a, %d %b %Y %X %z", time),
+            "Subject: Invoice",
+          ] );
+          my $message = new Mail::Internet (
+            'Header' => $header,
+            'Body' => [ $cust_bill->print_text ], #( date)
+          );
+          $message->smtpsend or die "Can't send invoice email!"; #die?  warn?
+
+        } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
+          open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
+          print LPR $cust_bill->print_text; #( date )
+          close LPR
+            or die $! ? "Error closing $lpr: $!"
+                         : "Exit status $? from $lpr";
+        }
 
 
-        my(%hash)=$cust_bill->hash;
+        my %hash = $cust_bill->hash;
         $hash{'printed'}++;
         $hash{'printed'}++;
-        my($new_cust_bill)=create FS::cust_bill(\%hash);
-        my($error)=$new_cust_bill->replace($cust_bill);
-        if ( $error ) {
-          warn "Error updating $cust_bill->printed: $error";
-        }
+        my $new_cust_bill = new FS::cust_bill(\%hash);
+        my $error = $new_cust_bill->replace($cust_bill);
+        warn "Error updating $cust_bill->printed: $error" if $error;
 
       }
 
 
       }
 
-    } elsif ( $self->getfield('payby') eq 'COMP' ) {
-      my($cust_pay) = create FS::cust_pay ( {
-         'invnum' => $cust_bill->getfield('invnum'),
+    } elsif ( $self->payby eq 'COMP' ) {
+      my $cust_pay = new FS::cust_pay ( {
+         'invnum' => $cust_bill->invnum,
          'paid' => $amount,
          '_date' => '',
          'payby' => 'COMP',
          'paid' => $amount,
          '_date' => '',
          'payby' => 'COMP',
-         'payinfo' => $self->getfield('payinfo'),
+         'payinfo' => $self->payinfo,
          'paybatch' => ''
       } );
          'paybatch' => ''
       } );
-      my($error)=$cust_pay->insert;
-      return 'Error COMPing invnum #' . $cust_bill->getfield('invnum') .
+      my $error = $cust_pay->insert;
+      return 'Error COMPing invnum #' . $cust_bill->invnum .
              ':' . $error if $error;
              ':' . $error if $error;
-    } elsif ( $self->getfield('payby') eq 'CARD' ) {
+
+    } elsif ( $self->payby eq 'CARD' ) {
 
       if ( $options{'batch_card'} ne 'yes' ) {
 
         return "Real time card processing not enabled!" unless $processor;
 
 
       if ( $options{'batch_card'} ne 'yes' ) {
 
         return "Real time card processing not enabled!" unless $processor;
 
-        if ( $processor =~ /cybercash/ ) {
+        if ( $processor =~ /^cybercash/ ) {
 
           #fix exp. date for cybercash
 
           #fix exp. date for cybercash
-          $self->getfield('paydate') =~ /^(\d+)\/\d*(\d{2})$/;
-          my($exp)="$1/$2";
+          #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
+          $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+          my $exp = "$2/$1";
 
 
-          my($paybatch)= $cust_bill->getfield('invnum') 
-                         '-' . time2str("%y%m%d%H%M%S",time);
+          my $paybatch = $cust_bill->invnum
+                         '-' . time2str("%y%m%d%H%M%S", time);
 
 
-          my($payname)= $self->getfield('payname') ||
-                        $self->getfield('first') . ' ' .$self->getfield('last');
+          my $payname = $self->payname ||
+                        $self->getfield('first'). ' '. $self->getfield('last');
 
 
-          my($address)= $self->getfield('address1');
-          $address .= ", " . $self->getfield('address2')
-            if $self->getfield('address2');
+          my $address = $self->address1;
+          $address .= ", ". $self->address2 if $self->address2;
 
 
-          my($country) = $self->getfield('country') eq 'US' ?
-                         'USA' : $self->getfield('country');
+          my $country = 'USA' if $self->country eq 'US';
 
 
-          my(@full_xaction)=($xaction,
+          my @full_xaction = ( $xaction,
             'Order-ID'     => $paybatch,
             'Amount'       => "usd $amount",
             'Card-Number'  => $self->getfield('payinfo'),
             'Order-ID'     => $paybatch,
             'Amount'       => "usd $amount",
             'Card-Number'  => $self->getfield('payinfo'),
@@ -696,7 +725,7 @@ sub collect {
             'Card-Exp'     => $exp,
           );
 
             'Card-Exp'     => $exp,
           );
 
-          my(%result);
+          my %result;
           if ( $processor eq 'cybercash2' ) {
             $^W=0; #CCLib isn't -w safe, ugh!
             %result = &CCLib::sendmserver(@full_xaction);
           if ( $processor eq 'cybercash2' ) {
             $^W=0; #CCLib isn't -w safe, ugh!
             %result = &CCLib::sendmserver(@full_xaction);
@@ -710,21 +739,21 @@ sub collect {
           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
-            my($cust_pay) = create FS::cust_pay ( {
-               'invnum'   => $cust_bill->getfield('invnum'),
+            my $cust_pay = new FS::cust_pay ( {
+               'invnum'   => $cust_bill->invnum,
                'paid'     => $amount,
                '_date'     => '',
                'payby'    => 'CARD',
                'paid'     => $amount,
                '_date'     => '',
                'payby'    => 'CARD',
-               'payinfo'  => $self->getfield('payinfo'),
+               'payinfo'  => $self->payinfo,
                'paybatch' => "$processor:$paybatch",
             } );
                'paybatch' => "$processor:$paybatch",
             } );
-            my($error)=$cust_pay->insert;
+            my $error = $cust_pay->insert;
             return 'Error applying payment, invnum #' . 
             return 'Error applying payment, invnum #' . 
-              $cust_bill->getfield('invnum') . ':' . $error if $error;
+              $cust_bill->invnum. ':'. $error if $error;
           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
                  || $options{'report_badcard'} ) {
              return 'Cybercash error, invnum #' . 
           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
                  || $options{'report_badcard'} ) {
              return 'Cybercash error, invnum #' . 
-               $cust_bill->getfield('invnum') . ':' . $result{'MErrMsg'};
+               $cust_bill->invnum. ':'. $result{'MErrMsg'};
           } else {
             return '';
           }
           } else {
             return '';
           }
@@ -735,8 +764,7 @@ sub collect {
 
       } else { #batch card
 
 
       } else { #batch card
 
-#       my($cust_pay_batch) = create FS::cust_pay_batch ( {
-       my($cust_pay_batch) = new FS::Record ('cust_pay_batch', {
+       my $cust_pay_batch = new FS::cust_pay_batch ( {
          'invnum'   => $cust_bill->getfield('invnum'),
          'custnum'  => $self->getfield('custnum'),
          'last'     => $self->getfield('last'),
          'invnum'   => $cust_bill->getfield('invnum'),
          'custnum'  => $self->getfield('custnum'),
          'last'     => $self->getfield('last'),
@@ -753,16 +781,19 @@ sub collect {
          'payname'  => $self->getfield('payname'),
          'amount'   => $amount,
        } );
          'payname'  => $self->getfield('payname'),
          'amount'   => $amount,
        } );
-#       my($error)=$cust_pay_batch->insert;
-       my($error)=$cust_pay_batch->add;
+       my $error = $cust_pay_batch->insert;
        return "Error adding to cust_pay_batch: $error" if $error;
 
       }
 
     } else {
        return "Error adding to cust_pay_batch: $error" if $error;
 
       }
 
     } else {
-      return "Unknown payment type ".$self->getfield('payby');
+      return "Unknown payment type ". $self->payby;
     }
 
     }
 
+
+
+
+
   }
   '';
 
   }
   '';
 
@@ -776,15 +807,14 @@ Returns the total owed for this customer on all invoices
 =cut
 
 sub total_owed {
 =cut
 
 sub total_owed {
-  my($self) = @_;
-  my($total_bill) = 0;
-  my($cust_bill);
-  foreach $cust_bill ( qsearch('cust_bill', {
-    'custnum' => $self->getfield('custnum'),
+  my $self = shift;
+  my $total_bill = 0;
+  foreach my $cust_bill ( qsearch('cust_bill', {
+    'custnum' => $self->custnum,
   } ) ) {
   } ) ) {
-    $total_bill += $cust_bill->getfield('owed');
+    $total_bill += $cust_bill->owed;
   }
   }
-  sprintf("%.2f",$total_bill);
+  sprintf( "%.2f", $total_bill );
 }
 
 =item total_credited
 }
 
 =item total_credited
@@ -794,15 +824,14 @@ Returns the total credits (see L<FS::cust_credit>) for this customer.
 =cut
 
 sub total_credited {
 =cut
 
 sub total_credited {
-  my($self) = @_;
-  my($total_credit) = 0;
-  my($cust_credit);
-  foreach $cust_credit ( qsearch('cust_credit', {
-    'custnum' => $self->getfield('custnum'),
+  my $self = shift;
+  my $total_credit = 0;
+  foreach my $cust_credit ( qsearch('cust_credit', {
+    'custnum' => $self->custnum,
   } ) ) {
   } ) ) {
-    $total_credit += $cust_credit->getfield('credited');
+    $total_credit += $cust_credit->credited;
   }
   }
-  sprintf("%.2f",$total_credit);
+  sprintf( "%.2f", $total_credit );
 }
 
 =item balance
 }
 
 =item balance
@@ -812,30 +841,122 @@ Returns the balance for this customer (total owed minus total credited).
 =cut
 
 sub balance {
 =cut
 
 sub balance {
-  my($self) = @_;
-  sprintf("%.2f",$self->total_owed - $self->total_credited);
+  my $self = shift;
+  sprintf( "%.2f", $self->total_owed - $self->total_credited );
+}
+
+=item invoicing_list [ ARRAYREF ]
+
+If an arguement is given, sets these email addresses as invoice recipients
+(see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
+(except as warnings), so use check_invoicing_list first.
+
+Returns a list of email addresses (with svcnum entries expanded).
+
+Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
+check it without disturbing anything by passing nothing.
+
+This interface may change in the future.
+
+=cut
+
+sub invoicing_list {
+  my( $self, $arrayref ) = @_;
+  if ( $arrayref ) {
+    my @cust_main_invoice;
+    if ( $self->custnum ) {
+      @cust_main_invoice = 
+        qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+    } else {
+      @cust_main_invoice = ();
+    }
+    foreach my $cust_main_invoice ( @cust_main_invoice ) {
+      #warn $cust_main_invoice->destnum;
+      unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
+        #warn $cust_main_invoice->destnum;
+        my $error = $cust_main_invoice->delete;
+        warn $error if $error;
+      }
+    }
+    if ( $self->custnum ) {
+      @cust_main_invoice = 
+        qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+    } else {
+      @cust_main_invoice = ();
+    }
+    foreach my $address ( @{$arrayref} ) {
+      unless ( grep { $address eq $_->address } @cust_main_invoice ) {
+        my $cust_main_invoice = new FS::cust_main_invoice ( {
+          'custnum' => $self->custnum,
+          'dest'    => $address,
+        } );
+        my $error = $cust_main_invoice->insert;
+        warn $error if $error;
+      } 
+    }
+  }
+  if ( $self->custnum ) {
+    map { $_->address }
+      qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
+  } else {
+    ();
+  }
+}
+
+=item check_invoicing_list ARRAYREF
+
+Checks these arguements as valid input for the invoicing_list method.  If there
+is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub check_invoicing_list {
+  my( $self, $arrayref ) = @_;
+  foreach my $address ( @{$arrayref} ) {
+    my $cust_main_invoice = new FS::cust_main_invoice ( {
+      'custnum' => $self->custnum,
+      'dest'    => $address,
+    } );
+    my $error = $self->custnum
+                ? $cust_main_invoice->check
+                : $cust_main_invoice->checkdest
+    ;
+    return $error if $error;
+  }
+  '';
 }
 
 =back
 
 }
 
 =back
 
+=head1 VERSION
+
+$Id: cust_main.pm,v 1.24 1999-07-20 10:37:05 ivan Exp $
+
 =head1 BUGS
 
 The delete method.
 
 =head1 BUGS
 
 The delete method.
 
-It doesn't properly override FS::Record yet.
-
-hfields should be removed.
+The delete method should possibly take an FS::cust_main object reference
+instead of a scalar customer number.
 
 Bill and collect options should probably be passed as references instead of a
 list.
 
 CyberCash v2 forces us to define some variables in package main.
 
 
 Bill and collect options should probably be passed as references instead of a
 list.
 
 CyberCash v2 forces us to define some variables in package main.
 
+There should probably be a configuration file with a list of allowed credit
+card types.
+
+CyberCash is the only processor.
+
+No multiple currency support (probably a larger project than just this module).
+
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
-L<FS::cust_main_county>, L<FS::UID>, schema.html from the base documentation.
+L<FS::cust_main_county>, L<FS::cust_main_invoice>,
+L<FS::UID>, schema.html from the base documentation.
 
 =head1 HISTORY
 
 
 =head1 HISTORY
 
@@ -868,7 +989,73 @@ enable cybercash, cybercash v3 support, don't need to import
 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
 
 $Log: cust_main.pm,v $
 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
 
 $Log: cust_main.pm,v $
-Revision 1.3  1998-11-13 09:56:54  ivan
+Revision 1.24  1999-07-20 10:37:05  ivan
+cleaned up the new one-screen signup bits in htdocs/edit/cust_main.cgi to
+prepare for a signup server
+
+Revision 1.23  1999/07/17 02:24:14  ivan
+bug noticed by Steve Gertz <sglist@hollywood.mwis.net>
+
+Revision 1.22  1999/04/15 16:44:36  ivan
+delete customers
+
+Revision 1.21  1999/04/14 07:47:53  ivan
+i18n fixes
+
+Revision 1.20  1999/04/10 08:35:14  ivan
+say what the unknown state/county/country are!
+
+Revision 1.19  1999/04/10 07:38:06  ivan
+_all_ check stuff with illegal data return the bad data too, to help debugging
+
+Revision 1.18  1999/04/10 06:54:11  ivan
+ditto
+
+Revision 1.17  1999/04/10 05:27:38  ivan
+display an illegal payby, to assist importing
+
+Revision 1.16  1999/04/07 14:32:19  ivan
+more &invoicing_list logic to skip searches when there is no custnum
+
+Revision 1.15  1999/04/07 13:41:54  ivan
+in &invoicing_list, don't search if there's no custnum yet
+
+Revision 1.14  1999/03/29 12:06:15  ivan
+buglet in email invoices fixed
+
+Revision 1.13  1999/02/28 20:09:03  ivan
+allow spaces in zip codes, for (at least) canada.  pointed out by
+Clayton Gray <clgray@bcgroup.net>
+
+Revision 1.12  1999/02/27 21:24:22  ivan
+parse paydate correctly for cybercash
+
+Revision 1.11  1999/02/23 08:09:27  ivan
+beginnings of one-screen new customer entry and some other miscellania
+
+Revision 1.10  1999/01/25 12:26:09  ivan
+yet more mod_perl stuff
+
+Revision 1.9  1999/01/18 09:22:41  ivan
+changes to track email addresses for email invoicing
+
+Revision 1.8  1998/12/29 11:59:39  ivan
+mostly properly OO, some work still to be done with svc_ stuff
+
+Revision 1.7  1998/12/16 09:58:52  ivan
+library support for editing email invoice destinations (not in sub collect yet)
+
+Revision 1.6  1998/11/18 09:01:42  ivan
+i18n! i18n!
+
+Revision 1.5  1998/11/15 11:23:14  ivan
+use FS::table_name for all searches to eliminate warnings,
+emit state/county when they don't match
+
+Revision 1.4  1998/11/15 05:30:48  ivan
+bugfix for new config layout
+
+Revision 1.3  1998/11/13 09:56:54  ivan
 change configuration file layout to support multiple distinct databases (with
 own set of config files, export, etc.)
 
 change configuration file layout to support multiple distinct databases (with
 own set of config files, export, etc.)