remove trancode from batching
[freeside.git] / FS / FS / cust_bill.pm
index 943c7b3..c15dad4 100644 (file)
@@ -1,17 +1,13 @@
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $conf $invoice_template $money_char );
-use vars qw( $lpr $invoice_from $smtpmachine );
-use vars qw( $processor );
-use vars qw( $xaction $E_NoErr );
-use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
+use vars qw( @ISA $conf $money_char );
 use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
-use Mail::Internet;
-use Mail::Header;
 use Text::Template;
+use FS::UID qw( datasrc );
 use FS::Record qw( qsearch qsearchs );
+use FS::Misc qw( send_email );
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
@@ -24,65 +20,10 @@ use FS::cust_bill_event;
 @ISA = qw( FS::Record );
 
 #ask FS::UID to run this stuff for us later
-$FS::UID::callback{'FS::cust_bill'} = sub { 
-
+FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
-
   $money_char = $conf->config('money_char') || '$';  
-
-  my @invoice_template = $conf->config('invoice_template')
-    or die "cannot load config file invoice_template";
-  $invoice_lines = 0;
-  foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
-    /invoice_lines\((\d+)\)/;
-    $invoice_lines += $1;
-  }
-  die "no invoice_lines() functions in template?" unless $invoice_lines;
-  $invoice_template = new Text::Template (
-    TYPE   => 'ARRAY',
-    SOURCE => [ map "$_\n", @invoice_template ],
-  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
-  $invoice_template->compile()
-    or die "can't compile template: $Text::Template::ERROR";
-
-  $lpr = $conf->config('lpr');
-  $invoice_from = $conf->config('invoice_from');
-  $smtpmachine = $conf->config('smtpmachine');
-
-  if ( $conf->exists('cybercash3.2') ) {
-    require CCMckLib3_2;
-      #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
-    require CCMckDirectLib3_2;
-      #qw(SendCC2_1Server);
-    require CCMckErrno3_2;
-      #qw(MCKGetErrorMessage $E_NoErr);
-    import CCMckErrno3_2 qw($E_NoErr);
-
-    my $merchant_conf;
-    ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
-    my $status = &CCMckLib3_2::InitConfig($merchant_conf);
-    if ( $status != $E_NoErr ) {
-      warn "CCMckLib3_2::InitConfig error:\n";
-      foreach my $key (keys %CCMckLib3_2::Config) {
-        warn "  $key => $CCMckLib3_2::Config{$key}\n"
-      }
-      my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
-      die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
-    }
-    $processor='cybercash3.2';
-  } elsif ( $conf->exists('business-onlinepayment') ) {
-    ( $bop_processor,
-      $bop_login,
-      $bop_password,
-      $bop_action,
-      @bop_options
-    ) = $conf->config('business-onlinepayment');
-    $bop_action ||= 'normal authorization';
-    eval "use Business::OnlinePayment";  
-    $processor="Business::OnlinePayment::$bop_processor";
-  }
-
-};
+} );
 
 =head1 NAME
 
@@ -220,7 +161,7 @@ sub check {
 
   $self->printed(0) if $self->printed eq '';
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item previous
@@ -383,35 +324,30 @@ emails or print.  See L<FS::cust_main_invoice>.
 =cut
 
 sub send {
-  my $self = shift;
-
-  #my @print_text = $cust_bill->print_text; #( date )
+  my($self,$template) = @_;
+  my @print_text = $self->print_text('', $template);
   my @invoicing_list = $self->cust_main->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' => [ $self->print_text ], #( date)
+
+  if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list  ) { #email
+
+    #better to notify this person than silence
+    @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
+
+    my $error = send_email(
+      'from'    => $conf->config('invoice_from'),
+      'to'      => [ grep { $_ ne 'POST' } @invoicing_list ],
+      'subject' => 'Invoice',
+      'body'    => \@print_text,
     );
-    $message->smtpsend
-      or return "(customer # ". $self->custnum. ") can't send invoice email".
-                " for ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
-                " to server $smtpmachine!";
+    return "can't send invoice: $error" if $error;
 
-  #} elsif ( grep { $_ eq 'POST' } @invoicing_list ) {
-  } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
+  }
+
+  if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
+    my $lpr = $conf->config('lpr');
     open(LPR, "|$lpr")
       or return "Can't open pipe to $lpr: $!";
-    print LPR $self->print_text; #( date )
+    print LPR @print_text;
     close LPR
       or return $! ? "Error closing $lpr: $!"
                    : "Exit status $? from $lpr";
@@ -421,6 +357,173 @@ sub send {
 
 }
 
+=item send_csv OPTIONS
+
+Sends invoice as a CSV data-file to a remote host with the specified protocol.
+
+Options are:
+
+protocol - currently only "ftp"
+server
+username
+password
+dir
+
+The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
+and YYMMDDHHMMSS is a timestamp.
+
+The fields of the CSV file is as follows:
+
+record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+
+=over 4
+
+=item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
+
+If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
+last five fields (B<pkg> through B<edate>) are irrelevant, and all other
+fields are filled in.
+
+If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
+first two fields (B<record_type> and B<invnum>) and the last five fields
+(B<pkg> through B<edate>) are filled in.
+
+=item invnum - invoice number
+
+=item custnum - customer number
+
+=item _date - invoice date
+
+=item charged - total invoice amount
+
+=item first - customer first name
+
+=item last - customer first name
+
+=item company - company name
+
+=item address1 - address line 1
+
+=item address2 - address line 1
+
+=item city
+
+=item state
+
+=item zip
+
+=item country
+
+=item pkg - line item description
+
+=item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
+
+=item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
+
+=item sdate - start date for recurring fee
+
+=item edate - end date for recurring fee
+
+=back
+
+=cut
+
+sub send_csv {
+  my($self, %opt) = @_;
+
+  #part one: create file
+
+  my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
+  mkdir $spooldir, 0700 unless -d $spooldir;
+
+  my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
+
+  open(CSV, ">$file") or die "can't open $file: $!";
+
+  eval "use Text::CSV_XS";
+  die $@ if $@;
+
+  my $csv = Text::CSV_XS->new({'always_quote'=>1});
+
+  my $cust_main = $self->cust_main;
+
+  $csv->combine(
+    'cust_bill',
+    $self->invnum,
+    $self->custnum,
+    time2str("%x", $self->_date),
+    sprintf("%.2f", $self->charged),
+    ( map { $cust_main->getfield($_) }
+        qw( first last company address1 address2 city state zip country ) ),
+    map { '' } (1..5),
+  ) or die "can't create csv";
+  print CSV $csv->string. "\n";
+
+  #new charges (false laziness w/print_text)
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+
+    my($pkg, $setup, $recur, $sdate, $edate);
+    if ( $cust_bill_pkg->pkgnum ) {
+    
+      ($pkg, $setup, $recur, $sdate, $edate) = (
+        $cust_bill_pkg->cust_pkg->part_pkg->pkg,
+        ( $cust_bill_pkg->setup != 0
+          ? sprintf("%.2f", $cust_bill_pkg->setup )
+          : '' ),
+        ( $cust_bill_pkg->recur != 0
+          ? sprintf("%.2f", $cust_bill_pkg->recur )
+          : '' ),
+        time2str("%x", $cust_bill_pkg->sdate),
+        time2str("%x", $cust_bill_pkg->edate),
+      );
+
+    } else { #pkgnum tax
+      next unless $cust_bill_pkg->setup != 0;
+      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
+                       ? ( $cust_bill_pkg->itemdesc || 'Tax' )
+                       : 'Tax';
+      ($pkg, $setup, $recur, $sdate, $edate) =
+        ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+    }
+
+    $csv->combine(
+      'cust_bill_pkg',
+      $self->invnum,
+      ( map { '' } (1..11) ),
+      ($pkg, $setup, $recur, $sdate, $edate)
+    ) or die "can't create csv";
+    print CSV $csv->string. "\n";
+
+  }
+
+  close CSV or die "can't close CSV: $!";
+
+  #part two: upload it
+
+  my $net;
+  if ( $opt{protocol} eq 'ftp' ) {
+    eval "use Net::FTP;";
+    die $@ if $@;
+    $net = Net::FTP->new($opt{server}) or die @$;
+  } else {
+    die "unknown protocol: $opt{protocol}";
+  }
+
+  $net->login( $opt{username}, $opt{password} )
+    or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
+
+  $net->binary or die "can't set binary mode";
+
+  $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
+
+  $net->put($file) or die "can't put $file: $!";
+
+  $net->quit;
+
+  unlink $file;
+
+}
+
 =item comp
 
 Pays this invoice with a compliemntary payment.  If there is an error,
@@ -443,215 +546,72 @@ sub comp {
 
 =item realtime_card
 
-Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
-See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
-for supproted processors.
+Attempts to pay this invoice with a credit card payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
 
 =cut
 
 sub realtime_card {
   my $self = shift;
-  my $cust_main = $self->cust_main;
-  my $amount = $self->owed;
-
-  unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
-    return "Real-time card processing not enabled (processor $processor)";
-  }
-  my $bop_processor = $1; #hmm?
-
-  my $address = $cust_main->address1;
-  $address .= ", ". $cust_main->address2 if $cust_main->address2;
-
-  #fix exp. date
-  #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
-  $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-  my $exp = "$2/$1";
-
-  my($payname, $payfirst, $paylast);
-  if ( $cust_main->payname ) {
-    $payname = $cust_main->payname;
-    $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
-      or do {
-              #$dbh->rollback if $oldAutoCommit;
-              return "Illegal payname $payname";
-            };
-    ($payfirst, $paylast) = ($1, $2);
-  } else {
-    $payfirst = $cust_main->getfield('first');
-    $paylast = $cust_main->getfield('first');
-    $payname =  "$payfirst $paylast";
-  }
-
-  my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
-  if ( $conf->exists('emailinvoiceauto')
-       || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
-    push @invoicing_list, $cust_main->default_invoicing_list;
-  }
-  my $email = $invoicing_list[0];
-
-  my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
-  
-  my $transaction =
-    new Business::OnlinePayment( $bop_processor, @bop_options );
-  $transaction->content(
-    'type'           => 'CC',
-    'login'          => $bop_login,
-    'password'       => $bop_password,
-    'action'         => $action1,
-    'description'    => 'Internet Services',
-    'amount'         => $amount,
-    'invoice_number' => $self->invnum,
-    'customer_id'    => $self->custnum,
-    'last_name'      => $paylast,
-    'first_name'     => $payfirst,
-    'name'           => $payname,
-    'address'        => $address,
-    'city'           => $cust_main->city,
-    'state'          => $cust_main->state,
-    'zip'            => $cust_main->zip,
-    'country'        => $cust_main->country,
-    'card_number'    => $cust_main->payinfo,
-    'expiration'     => $exp,
-    'referer'        => 'http://cleanwhisker.420.am/',
-    'email'          => $email,
-  );
-  $transaction->submit();
-
-  if ( $transaction->is_success() && $action2 ) {
-    my $auth = $transaction->authorization;
-    my $ordernum = $transaction->order_number;
-    #warn "********* $auth ***********\n";
-    #warn "********* $ordernum ***********\n";
-    my $capture =
-      new Business::OnlinePayment( $bop_processor, @bop_options );
-
-    $capture->content(
-      action         => $action2,
-      login          => $bop_login,
-      password       => $bop_password,
-      order_number   => $ordernum,
-      amount         => $amount,
-      authorization  => $auth,
-      description    => 'Internet Services',
-    );
-
-    $capture->submit();
-
-    unless ( $capture->is_success ) {
-      my $e = "Authorization sucessful but capture failed, invnum #".
-              $self->invnum. ': '.  $capture->result_code.
-              ": ". $capture->error_message;
-      warn $e;
-      return $e;
-    }
-
-  }
-
-  if ( $transaction->is_success() ) {
-
-    my $cust_pay = new FS::cust_pay ( {
-       'invnum'   => $self->invnum,
-       'paid'     => $amount,
-       '_date'     => '',
-       'payby'    => 'CARD',
-       'payinfo'  => $cust_main->payinfo,
-       'paybatch' => "$processor:". $transaction->authorization,
-    } );
-    my $error = $cust_pay->insert;
-    if ( $error ) {
-      # gah, even with transactions.
-      my $e = 'WARNING: Card debited but database not updated - '.
-              'error applying payment, invnum #' . $self->invnum.
-              " ($processor): $error";
-      warn $e;
-      return $e;
-    } else {
-      return '';
-    }
-  #} elsif ( $options{'report_badcard'} ) {
-  } else {
-    return "$processor error, invnum #". $self->invnum. ': '.
-           $transaction->result_code. ": ". $transaction->error_message;
-  }
-
+  $self->realtime_bop( 'CC', @_ );
 }
 
-=item realtime_card_cybercash
+=item realtime_ach
 
-Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
+Attempts to pay this invoice with an electronic check (ACH) payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
 
 =cut
 
-sub realtime_card_cybercash {
+sub realtime_ach {
   my $self = shift;
-  my $cust_main = $self->cust_main;
-  my $amount = $self->owed;
+  $self->realtime_bop( 'ECHECK', @_ );
+}
 
-  return "CyberCash CashRegister real-time card processing not enabled!"
-    unless $processor eq 'cybercash3.2';
+=item realtime_lec
 
-  my $address = $cust_main->address1;
-  $address .= ", ". $cust_main->address2 if $cust_main->address2;
+Attempts to pay this invoice with phone bill (LEC) payment via a
+Business::OnlinePayment realtime gateway.  See
+http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
+for supported processors.
 
-  #fix exp. date
-  #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
-  $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
-  my $exp = "$2/$1";
+=cut
 
-  #
+sub realtime_lec {
+  my $self = shift;
+  $self->realtime_bop( 'LEC', @_ );
+}
 
-  my $paybatch = $self->invnum. 
-                  '-' . time2str("%y%m%d%H%M%S", time);
+sub realtime_bop {
+  my( $self, $method ) = @_;
 
-  my $payname = $cust_main->payname ||
-                $cust_main->getfield('first').' '.$cust_main->getfield('last');
+  my $cust_main = $self->cust_main;
+  my $amount = $self->owed;
 
-  my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
+  my $description = 'Internet Services';
+  if ( $conf->exists('business-onlinepayment-description') ) {
+    my $dtempl = $conf->config('business-onlinepayment-description');
+
+    my $agent_obj = $cust_main->agent
+      or die "can't retreive agent for $cust_main (agentnum ".
+             $cust_main->agentnum. ")";
+    my $agent = $agent_obj->agent;
+    my $pkgs = join(', ',
+      map { $_->cust_pkg->part_pkg->pkg }
+        grep { $_->pkgnum } $self->cust_bill_pkg
+    );
+    $description = eval qq("$dtempl");
+  }
 
-  my @full_xaction = ( $xaction,
-    'Order-ID'     => $paybatch,
-    'Amount'       => "usd $amount",
-    'Card-Number'  => $cust_main->getfield('payinfo'),
-    'Card-Name'    => $payname,
-    'Card-Address' => $address,
-    'Card-City'    => $cust_main->getfield('city'),
-    'Card-State'   => $cust_main->getfield('state'),
-    'Card-Zip'     => $cust_main->getfield('zip'),
-    'Card-Country' => $country,
-    'Card-Exp'     => $exp,
+  $cust_main->realtime_bop($method, $amount,
+    'description' => $description,
+    'invnum'      => $self->invnum,
   );
 
-  my %result;
-  %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
-  
-  if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
-    my $cust_pay = new FS::cust_pay ( {
-       'invnum'   => $self->invnum,
-       'paid'     => $amount,
-       '_date'     => '',
-       'payby'    => 'CARD',
-       'payinfo'  => $cust_main->payinfo,
-       'paybatch' => "$processor:$paybatch",
-    } );
-    my $error = $cust_pay->insert;
-    if ( $error ) {
-      # gah, even with transactions.
-      my $e = 'WARNING: Card debited but database not updated - '.
-              'error applying payment, invnum #' . $self->invnum.
-              " (CyberCash Order-ID $paybatch): $error";
-      warn $e;
-      return $e;
-    } else {
-      return '';
-    }
-#  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
-#            || $options{'report_badcard'}
-#          ) {
-  } else {
-     return 'Cybercash error, invnum #' . 
-       $self->invnum. ':'. $result{'MErrMsg'};
-  }
-
 }
 
 =item batch_card
@@ -676,14 +636,15 @@ sub batch_card {
     'state'    => $cust_main->getfield('state'),
     'zip'      => $cust_main->getfield('zip'),
     'country'  => $cust_main->getfield('country'),
-    'trancode' => 77,
     'cardnum'  => $cust_main->getfield('payinfo'),
     'exp'      => $cust_main->getfield('paydate'),
     'payname'  => $cust_main->getfield('payname'),
     'amount'   => $self->owed,
   } );
-  $cust_pay_batch->insert;
+  my $error = $cust_pay_batch->insert;
+  die $error if $error;
 
+  '';
 }
 
 =item print_text [TIME];
@@ -699,12 +660,12 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 sub print_text {
 
-  my( $self, $today ) = ( shift, shift );
+  my( $self, $today, $template ) = @_;
   $today ||= time;
 #  my $invnum = $self->invnum;
   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname;
+    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
@@ -731,33 +692,50 @@ sub print_text {
   }
 
   #new charges
-  foreach ( $self->cust_bill_pkg ) {
+  foreach my $cust_bill_pkg (
+    ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
+    ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
+  ) {
 
-    if ( $_->pkgnum ) {
+    if ( $cust_bill_pkg->pkgnum ) {
 
-      my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
-      my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
-      my($pkg)=$part_pkg->pkg;
+      my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
+      my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
+      my $pkg = $part_pkg->pkg;
 
-      if ( $_->setup != 0 ) {
-        push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
+      if ( $cust_bill_pkg->setup != 0 ) {
+        push @buf, [ "$pkg Setup",
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
       }
 
-      if ( $_->recur != 0 ) {
+      if ( $cust_bill_pkg->recur != 0 ) {
         push @buf, [
-          "$pkg (" . time2str("%x",$_->sdate) . " - " .
-                                time2str("%x",$_->edate) . ")",
-          $money_char. sprintf("%10.2f",$_->recur)
+          "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
+                                time2str("%x", $cust_bill_pkg->edate) . ")",
+          $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
       }
 
-    } else { #pkgnum Tax
-      push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
-        if $_->setup != 0;
+      push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
+
+    } else { #pkgnum tax or one-shot line item
+      my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
+                     ? ( $cust_bill_pkg->itemdesc || 'Tax' )
+                     : 'Tax';
+      if ( $cust_bill_pkg->setup != 0 ) {
+        push @buf, [ $itemdesc,
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
+      }
+      if ( $cust_bill_pkg->recur != 0 ) {
+        push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
+                                  . time2str("%x", $cust_bill_pkg->edate). ")",
+                     $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
+                   ];
+      }
     }
   }
 
@@ -808,20 +786,43 @@ sub print_text {
   push @buf,['Balance Due', $money_char. 
     sprintf("%10.2f", $balance_due ) ];
 
+  #create the template
+  my $templatefile = 'invoice_template';
+  $templatefile .= "_$template" if $template;
+  my @invoice_template = $conf->config($templatefile)
+  or die "cannot load config file $templatefile";
+  $invoice_lines = 0;
+  my $wasfunc = 0;
+  foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
+    /invoice_lines\((\d*)\)/;
+    $invoice_lines += $1 || scalar(@buf);
+    $wasfunc=1;
+  }
+  die "no invoice_lines() functions in template?" unless $wasfunc;
+  my $invoice_template = new Text::Template (
+    TYPE   => 'ARRAY',
+    SOURCE => [ map "$_\n", @invoice_template ],
+  ) or die "can't create new Text::Template object: $Text::Template::ERROR";
+  $invoice_template->compile()
+    or die "can't compile template: $Text::Template::ERROR";
+
   #setup template variables
-  
   package FS::cust_bill::_template; #!
-  use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
+  use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
 
   $invnum = $self->invnum;
   $date = $self->_date;
   $page = 1;
+  $agent = $self->cust_main->agent->agent;
 
-  $total_pages =
-    int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
-  $total_pages++
-    if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
-
+  if ( $FS::cust_bill::invoice_lines ) {
+    $total_pages =
+      int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
+    $total_pages++
+      if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
+  } else {
+    $total_pages = 1;
+  }
 
   #format address (variable for the template)
   my $l = 0;
@@ -844,24 +845,24 @@ sub print_text {
   $FS::cust_bill::_template::address[$l++] = $cust_main->country
     unless $cust_main->country eq 'US';
 
-  #overdue? (variable for the template)
-  $FS::cust_bill::_template::overdue = ( 
-    $balance_due > 0
-    && $today > $self->_date 
-#    && $self->printed > 1
-    && $self->printed > 0
-  );
+       #  #overdue? (variable for the template)
+       #  $FS::cust_bill::_template::overdue = ( 
+       #    $balance_due > 0
+       #    && $today > $self->_date 
+       ##    && $self->printed > 1
+       #    && $self->printed > 0
+       #  );
 
   #and subroutine for the template
-
   sub FS::cust_bill::_template::invoice_lines {
-    my $lines = shift;
+    my $lines = shift || scalar(@buf);
     map { 
       scalar(@buf) ? shift @buf : [ '', '' ];
     }
     ( 1 .. $lines );
   }
-    
+
+  #and fill it in
   $FS::cust_bill::_template::page = 1;
   my $lines;
   my @collect;
@@ -878,10 +879,6 @@ sub print_text {
 
 =back
 
-=head1 VERSION
-
-$Id: cust_bill.pm,v 1.19 2002-02-12 18:56:16 ivan Exp $
-
 =head1 BUGS
 
 The delete method.