doc
[freeside.git] / FS / FS / cust_bill.pm
index f536c97..68571f7 100644 (file)
@@ -12,11 +12,12 @@ use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
 use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps do_print );
+use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_main_Mixin;
 use FS::cust_main;
 use FS::cust_bill_pkg;
+use FS::cust_bill_pkg_display;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
@@ -226,7 +227,31 @@ Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
 
 sub cust_bill_pkg {
   my $self = shift;
-  qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
+  qsearch(
+    { 'table'    => 'cust_bill_pkg',
+      'hashref'  => { 'invnum' => $self->invnum },
+      'order_by' => 'ORDER BY billpkgnum',
+    }
+  );
+}
+
+=item cust_bill_pkg_pkgnum PKGNUM
+
+Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
+specified pkgnum.
+
+=cut
+
+sub cust_bill_pkg_pkgnum {
+  my( $self, $pkgnum ) = @_;
+  qsearch(
+    { 'table'    => 'cust_bill_pkg',
+      'hashref'  => { 'invnum' => $self->invnum,
+                      'pkgnum' => $pkgnum,
+                    },
+      'order_by' => 'ORDER BY billpkgnum',
+    }
+  );
 }
 
 =item cust_pkg
@@ -426,6 +451,38 @@ sub cust_credited {
   ;
 }
 
+=item cust_bill_pay_pkgnum PKGNUM
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+with matching pkgnum.
+
+=cut
+
+sub cust_bill_pay_pkgnum {
+  my( $self, $pkgnum ) = @_;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+                                'pkgnum' => $pkgnum,
+                              }
+           );
+}
+
+=item cust_credited_pkgnum PKGNUM
+
+Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+with matching pkgnum.
+
+=cut
+
+sub cust_credited_pkgnum {
+  my( $self, $pkgnum ) = @_;
+  sort { $a->_date <=> $b->_date }
+    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+                                   'pkgnum' => $pkgnum,
+                                 }
+           );
+}
+
 =item tax
 
 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
@@ -459,6 +516,21 @@ sub owed {
   $balance;
 }
 
+sub owed_pkgnum {
+  my( $self, $pkgnum ) = @_;
+
+  #my $balance = $self->charged;
+  my $balance = 0;
+  $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
+
+  $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
+  $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
+
+  $balance = sprintf( "%.2f", $balance);
+  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+  $balance;
+}
+
 =item apply_payments_and_credits
 
 =cut
@@ -482,6 +554,13 @@ sub apply_payments_and_credits {
   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
+  if ( $conf->exists('pkg-balances') ) {
+    # limit @payments & @credits to those w/ a pkgnum grepped from $self
+    my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
+    @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
+    @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
+  }
+
   while ( $self->owed > 0 and ( @payments || @credits ) ) {
 
     my $app = '';
@@ -519,28 +598,39 @@ sub apply_payments_and_credits {
       die "guru meditation #12 and 35";
     }
 
+    my $unapp_amount;
     if ( $app eq 'pay' ) {
 
       my $payment = shift @payments;
-
-      $app = new FS::cust_bill_pay {
-        'paynum'  => $payment->paynum,
-       'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
-      };
+      $unapp_amount = $payment->unapplied;
+      $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
+      $app->pkgnum( $payment->pkgnum )
+        if $conf->exists('pkg-balances') && $payment->pkgnum;
 
     } elsif ( $app eq 'credit' ) {
 
       my $credit = shift @credits;
-
-      $app = new FS::cust_credit_bill {
-        'crednum' => $credit->crednum,
-       'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
-      };
+      $unapp_amount = $credit->credited;
+      $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
+      $app->pkgnum( $credit->pkgnum )
+        if $conf->exists('pkg-balances') && $credit->pkgnum;
 
     } else {
       die "guru meditation #12 and 35";
     }
 
+    my $owed;
+    if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
+      warn "owed_pkgnum ". $app->pkgnum;
+      $owed = $self->owed_pkgnum($app->pkgnum);
+    } else {
+      $owed = $self->owed;
+    }
+    next unless $owed > 0;
+
+    warn "min ( $unapp_amount, $owed )\n";
+    $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
+
     $app->invnum( $self->invnum );
 
     my $error = $app->insert;
@@ -557,19 +647,27 @@ sub apply_payments_and_credits {
 
 }
 
-=item generate_email PARAMHASH
+=item generate_email OPTION => VALUE ...
 
-PARAMHASH can contain the following:
+Options:
 
 =over 4
 
-=item from       => sender address, required
+=item from
+
+sender address, required
 
-=item tempate    => alternate template name, optional
+=item tempate
 
-=item print_text => text attachment arrayref, optional
+alternate template name, optional
 
-=item subject    => email subject, optional
+=item print_text
+
+text attachment arrayref, optional
+
+=item subject
+
+email subject, optional
 
 =back
 
@@ -591,6 +689,8 @@ sub generate_email {
     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
   );
 
+  my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
+
   if (ref($args{'to'}) eq 'ARRAY') {
     $return{'to'} = $args{'to'};
   } else {
@@ -629,7 +729,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
-        $data = [ $self->print_text('', $args{'template'}) ];
+        $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
       }
 
     }
@@ -646,21 +746,22 @@ sub generate_email {
     my $from = $1 || 'example.com';
     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
 
-    my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
-    my $file;
+    my $logo;
+    my $agentnum = $self->cust_main->agentnum;
     if ( defined($args{'template'}) && length($args{'template'})
-         && -e "$path/logo_". $args{'template'}. ".png"
+         && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
        )
     {
-      $file = "$path/logo_". $args{'template'}. ".png";
+      $logo = 'logo_'. $args{'template'}. '.png';
     } else {
-      $file = "$path/logo.png";
+      $logo = "logo.png";
     }
+    my $image_data = $conf->config_binary( $logo, $agentnum);
 
     my $image = build MIME::Entity
       'Type'       => 'image/png',
       'Encoding'   => 'base64',
-      'Path'       => $file,
+      'Data'       => $image_data,
       'Filename'   => 'logo.png',
       'Content-ID' => "<$content_id>",
     ;
@@ -675,7 +776,11 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $self->print_html('', $args{'template'}, $content_id),
+                         $self->print_html({ time          => '',
+                                             template      => $args{'template'},
+                                             cid           => $content_id,
+                                             %cdrs,
+                                          }),
                          '  </body>',
                          '</html>',
                        ],
@@ -683,6 +788,21 @@ sub generate_email {
       #'Filename'    => 'invoice.pdf',
     );
 
+    my @otherparts = ();
+    if ( $self->cust_main->email_csv_cdr ) {
+
+      push @otherparts, build MIME::Entity
+        'Type'        => 'text/csv',
+        'Encoding'    => '7bit',
+        'Data'        => [ map { "$_\n" }
+                             $self->call_details('prepend_billed_number' => 1)
+                         ],
+        'Disposition' => 'attachment',
+        'Filename'    => 'usage-'. $self->invnum. '.csv',
+      ;
+
+    }
+
     if ( $conf->exists('invoice_email_pdf') ) {
 
       #attaching pdf too:
@@ -708,9 +828,9 @@ sub generate_email {
 
       $related->add_part($image);
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
 
-      $return{'mimeparts'} = [ $related, $pdf ];
+      $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
     } else {
 
@@ -722,7 +842,7 @@ sub generate_email {
       #   image/png
 
       $return{'content-type'} = 'multipart/related';
-      $return{'mimeparts'} = [ $alternative, $image ];
+      $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
       #$return{'disposition'} = 'inline';
 
@@ -736,7 +856,7 @@ sub generate_email {
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
-        { $self->mimebuild_pdf('', $args{'template'}) }
+        { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
       ];
     }
   
@@ -756,7 +876,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
-        $return{'body'} = [ $self->print_text('', $args{'template'}) ];
+        $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
       }
 
     }
@@ -781,7 +901,7 @@ sub mimebuild_pdf {
     'Encoding'    => 'base64',
     'Data'        => [ $self->print_pdf(@_) ],
     'Disposition' => 'attachment',
-    'Filename'    => 'invoice.pdf',
+    'Filename'    => 'invoice-'. $self->invnum. '.pdf',
   );
 }
 
@@ -798,6 +918,9 @@ single agent) or an arrayref of agentnums.
 
 INVOICE_FROM, if specified, overrides the default email invoice From: address.
 
+AMOUNT, if specified, only sends the invoice if the total amount owed on this
+invoice and all older invoices is greater than the specified amount.
+
 =cut
 
 sub queueable_send {
@@ -826,17 +949,26 @@ sub send {
   my $invoice_from =
     scalar(@_)
       ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+      : ( $self->_agent_invoice_from ||    #XXX should go away
+          $conf->config('invoice_from', $self->cust_main->agentnum )
+        );
+
+  my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+
+  return ''
+    unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
 
   my @invoicing_list = $self->cust_main->invoicing_list;
 
+  #$self->email_invoice($template, $invoice_from)
   $self->email($template, $invoice_from)
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
+  #$self->print_invoice($template)
   $self->print($template)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-  $self->fax($template)
+  $self->fax_invoice($template)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
@@ -868,13 +1000,17 @@ sub queueable_email {
 
 }
 
+#sub email_invoice {
 sub email {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
   my $invoice_from =
     scalar(@_)
       ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+      : ( $self->_agent_invoice_from ||    #XXX should go away
+          $conf->config('invoice_from', $self->cust_main->agentnum )
+        );
+
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
@@ -882,10 +1018,13 @@ sub email {
   #better to notify this person than silence
   @invoicing_list = ($invoice_from) unless @invoicing_list;
 
+  my $subject = $self->email_subject($template);
+
   my $error = send_email(
     $self->generate_email(
       'from'       => $invoice_from,
       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+      'subject'    => $subject,
       'template'   => $template,
     )
   );
@@ -894,6 +1033,24 @@ sub email {
 
 }
 
+sub email_subject {
+  my $self = shift;
+
+  #my $template = scalar(@_) ? shift : '';
+  #per-template?
+
+  my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+                || 'Invoice';
+
+  my $cust_main = $self->cust_main;
+  my $name = $cust_main->name;
+  my $name_short = $cust_main->name_short;
+  my $invoice_number = $self->invnum;
+  my $invoice_date = $self->_date_pretty;
+
+  eval qq("$subject");
+}
+
 =item lpr_data [ TEMPLATENAME ]
 
 Returns the postscript or plaintext for this invoice as an arrayref.
@@ -917,6 +1074,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
+#sub print_invoice {
 sub print {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
@@ -924,7 +1082,7 @@ sub print {
   do_print $self->lpr_data($template);
 }
 
-=item fax [ TEMPLATENAME ] 
+=item fax_invoice [ TEMPLATENAME ] 
 
 Faxes this invoice.
 
@@ -932,7 +1090,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
-sub fax {
+sub fax_invoice {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
 
@@ -949,6 +1107,46 @@ sub fax {
 
 }
 
+=item ftp_invoice [ TEMPLATENAME ] 
+
+Sends this invoice data via FTP.
+
+TEMPLATENAME is unused?
+
+=cut
+
+sub ftp_invoice {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  $self->send_csv(
+    'protocol'   => 'ftp',
+    'server'     => $conf->config('cust_bill-ftpserver'),
+    'username'   => $conf->config('cust_bill-ftpusername'),
+    'password'   => $conf->config('cust_bill-ftppassword'),
+    'dir'        => $conf->config('cust_bill-ftpdir'),
+    'format'     => $conf->config('cust_bill-ftpformat'),
+  );
+}
+
+=item spool_invoice [ TEMPLATENAME ] 
+
+Spools this invoice data (see L<FS::spool_csv>)
+
+TEMPLATENAME is unused?
+
+=cut
+
+sub spool_invoice {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  $self->spool_csv(
+    'format'       => $conf->config('cust_bill-spoolformat'),
+    'agent_spools' => $conf->exists('cust_bill-spoolagent'),
+  );
+}
+
 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
 
 Like B<send>, but only sends the invoice if it is the newest open invoice for
@@ -1347,7 +1545,7 @@ sub print_csv {
       if ( $cust_bill_pkg->pkgnum ) {
       
         ($pkg, $setup, $recur, $sdate, $edate) = (
-          $cust_bill_pkg->cust_pkg->part_pkg->pkg,
+          $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
             ? sprintf("%.2f", $cust_bill_pkg->setup )
             : '' ),
@@ -1364,11 +1562,9 @@ sub print_csv {
   
       } 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), '', '', '' );
+        $pkg = $cust_bill_pkg->desc;
+        $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
+        ( $sdate, $edate ) = ( '', '' );
       }
   
       $csv->combine(
@@ -1468,7 +1664,7 @@ sub realtime_bop {
              $cust_main->agentnum. ")";
     my $agent = $agent_obj->agent;
     my $pkgs = join(', ',
-      map { $_->cust_pkg->part_pkg->pkg }
+      map { $_->part_pkg->pkg }
         grep { $_->pkgnum } $self->cust_bill_pkg
     );
     $description = eval qq("$dtempl");
@@ -1520,11 +1716,12 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 =cut
 
 sub print_text {
-  my( $self, $today, $template ) = @_;
+  my( $self, $today, $template, %opt ) = @_;
 
   my %params = ( 'format' => 'template' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
+  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
 
   $self->print_generic( %params );
 }
@@ -1545,12 +1742,12 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 =cut
 
 sub print_latex {
-
-  my( $self, $today, $template ) = @_;
+  my( $self, $today, $template, %opt ) = @_;
 
   my %params = ( 'format' => 'latex' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
+  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
 
   $template ||= $self->_agent_template;
 
@@ -1561,11 +1758,13 @@ sub print_latex {
                            UNLINK   => 0,
                          ) or die "can't open temp file: $!\n";
 
-  if ($template && $conf->exists("logo_${template}.eps")) {
-    print $lh $conf->config_binary("logo_${template}.eps")
+  my $agentnum = $self->cust_main->agentnum;
+
+  if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
+    print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
       or die "can't write temp file: $!\n";
-  }else{
-    print $lh $conf->config_binary('logo.eps')
+  } else {
+    print $lh $conf->config_binary('logo.eps', $agentnum)
       or die "can't write temp file: $!\n";
   }
   close $lh;
@@ -1606,8 +1805,12 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 cid - 
 
+unsquelch_cdr - overrides any per customer cdr squelching when true
+
 =cut
 
+#what's with all the sprintf('%10.2f')'s in here?  will it cause any
+# (alignment?) problems to change them all to '%.2f' ?
 sub print_generic {
 
   my( $self, %params ) = @_;
@@ -1621,8 +1824,8 @@ sub print_generic {
 
   my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
-
+    unless $cust_main->payname
+        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
 
   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
                      'html'     => [ '<%=', '%>' ],
@@ -1663,6 +1866,7 @@ sub print_generic {
                  'footer'        => sub { map "$_", @_ },
                  'smallfooter'   => sub { map "$_", @_ },
                  'returnaddress' => sub { map "$_", @_ },
+                 'coupon'        => sub { map "$_", @_ },
                },
     'html'  => {
                  'notes' =>
@@ -1691,9 +1895,11 @@ sub print_generic {
                        s/~/&nbsp;/g;
                        s/\\\\\*?\s*$/<BR>/;
                        s/\\hyphenation\{[\w\s\-]+}//;
+                       s/\\([&])/$1/g;
                        $_;
                      }  @_
                    },
+                 'coupon'        => sub { "" },
                },
     'template' => {
                  'notes' =>
@@ -1723,6 +1929,7 @@ sub print_generic {
                        $_;
                      }  @_
                    },
+                 'coupon'        => sub { "" },
                },
   );
 
@@ -1740,7 +1947,7 @@ sub print_generic {
                          );
   my $escape_function = $escape_functions{$format};
 
-  my %date_formats = ( 'latex'    => '%b, %o, %Y',
+  my %date_formats = ( 'latex'    => '%b %o, %Y',
                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
                        'template' => '%s',
                      );
@@ -1783,15 +1990,19 @@ sub print_generic {
                                                )
                          )
           );
-  } elsif ( grep /\S/, $conf->config('company_address') ) {
+  } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
 
-    $returnaddress = join( "\n", $conf->config('company_address') );
-
-    $returnaddress =
-      join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
-                            $conf->config('company_address')
-          )
-        if $format eq 'latex';
+    my $convert_map = $convert_maps{$format}{'returnaddress'};
+    $returnaddress = join( "\n", &$convert_map(
+                                   map { s/( {2,})/'~' x length($1)/eg;
+                                         s/$/\\\\\*/;
+                                         $_
+                                       }
+                                     ( $conf->config('company_name', $self->cust_main->agentnum),
+                                       $conf->config('company_address', $self->cust_main->agentnum),
+                                     )
+                                 )
+                     );
 
   } else {
 
@@ -1804,13 +2015,14 @@ sub print_generic {
   }
 
   my %invoice_data = (
-    'company_name'    => scalar( $conf->config('company_name') ),
-    'company_address' => join("\n", $conf->config('company_address') ). "\n",
-    'custnum'         => $self->custnum,
+    'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
+    'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
+    'custnum'         => $cust_main->display_custnum,
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
     'today'           => time2str('%b %o, %Y', $today),
     'agent'           => &$escape_function($cust_main->agent->agent),
+    'agent_custid'    => &$escape_function($cust_main->agent_custid),
     'payname'         => &$escape_function($cust_main->payname),
     'company'         => &$escape_function($cust_main->company),
     'address1'        => &$escape_function($cust_main->address1),
@@ -1818,21 +2030,34 @@ sub print_generic {
     'city'            => &$escape_function($cust_main->city),
     'state'           => &$escape_function($cust_main->state),
     'zip'             => &$escape_function($cust_main->zip),
+    'fax'             => &$escape_function($cust_main->fax),
     'returnaddress'   => $returnaddress,
-    'quantity'        => 1,
+    #'quantity'        => 1,
     'terms'           => $self->terms,
-    'template'        => $params{'template'},
+    'template'        => $template, #params{'template'},
     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
     # better hang on to conf_dir for a while
     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
     'page'            => 1,
     'total_pages'     => 1,
+    'current_charges' => sprintf("%.2f", $self->charged),
+    'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
+    'ship_enable'     => $conf->exists('invoice-ship_address'),
+    'unitprices'      => $conf->exists('invoice-unitprice'),
   );
 
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+  foreach ( qw( contact company address1 address2 city state zip country fax) ){
+    my $method = $prefix.$_;
+    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+  }
+  $invoice_data{'ship_country'} = ''
+    if ( $invoice_data{'ship_country'} eq $countrydefault );
+  
   $invoice_data{'cid'} = $params{'cid'}
     if $params{'cid'};
 
-  my $countrydefault = $conf->config('countrydefault') || 'US';
   if ( $cust_main->country eq $countrydefault ) {
     $invoice_data{'country'} = '';
   } else {
@@ -1860,15 +2085,28 @@ sub print_generic {
   push @address, ''
     while (scalar(@address) < 5);
 
+  $invoice_data{'logo_file'} = $params{'logo_file'}
+    if $params{'logo_file'};
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
+  #my $balance_due = $self->owed + $pr_total - $cr_total;
+  my $balance_due = $self->owed + $pr_total;
+  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
+  $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
+
+  my $agentnum = $self->cust_main->agentnum;
+
   #do variable substitution in notes, footer, smallfooter
-  foreach my $include (qw( notes footer smallfooter )) {
+  foreach my $include (qw( notes footer smallfooter coupon )) {
 
     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
     my @inc_src;
 
-    if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
+    if ( $conf->exists($inc_file, $agentnum)
+         && length( $conf->config($inc_file, $agentnum) ) ) {
 
-      @inc_src = $conf->config($inc_file);
+      @inc_src = $conf->config($inc_file, $agentnum);
 
     } else {
 
@@ -1880,7 +2118,7 @@ sub print_generic {
                        s/--\@\]/$delimiters{$format}[1]/g;
                        $_;
                      } 
-                 &$convert_map( $conf->config($inc_file) );
+                 &$convert_map( $conf->config($inc_file, $agentnum) );
 
     }
 
@@ -1907,18 +2145,13 @@ sub print_generic {
       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
       : $nbsp;
 
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
-  #my $balance_due = $self->owed + $pr_total - $cr_total;
-  my $balance_due = $self->owed + $pr_total;
-
   my %money_chars = ( 'latex'    => '',
                       'html'     => $conf->config('money_char') || '$',
                       'template' => '',
                     );
   my $money_char = $money_chars{$format};
 
-  my %other_money_chars = ( 'latex'    => '\dollar ',
+  my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
                             'html'     => $conf->config('money_char') || '$',
                             'template' => '',
                           );
@@ -1939,46 +2172,62 @@ sub print_generic {
                                             sprintf('%.2f', $pr_total),
                          };
 
+  my $taxtotal = 0;
+  my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
+                      'subtotal'    => $taxtotal }; # adjusted below
+
+  my $adjusttotal = 0;
+  my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
+                         'subtotal'    => 0 }; # adjusted below
+
+  my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  my $late_sections = [];
   if ( $multisection ) {
-    push @sections, $self->_items_sections;
+    push @sections, $self->_items_sections( $late_sections );
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
 
-  foreach my $line_item ( $self->_items_previous ) {
-    my $detail = {
-      ext_description => [],
-    };
-    $detail->{'ref'} = $line_item->{'pkgnum'};
-    $detail->{'quantity'} = 1;
-    $detail->{'section'} = $previous_section;
-    $detail->{'description'} = &$escape_function($line_item->{'description'});
-    if ( exists $line_item->{'ext_description'} ) {
-      @{$detail->{'ext_description'}} = map {
-        &$escape_function($_);
-      } @{$line_item->{'ext_description'}};
-    }
-    {
-      my $money = $old_latex ? '' : $money_char;
-      $detail->{'amount'} = $money. $line_item->{'amount'};
+  unless (    $conf->exists('disable_previous_balance')
+           || $conf->exists('previous_balance-summary_only')
+         )
+  {
+
+    foreach my $line_item ( $self->_items_previous ) {
+
+      my $detail = {
+        ext_description => [],
+      };
+      $detail->{'ref'} = $line_item->{'pkgnum'};
+      $detail->{'quantity'} = 1;
+      $detail->{'section'} = $previous_section;
+      $detail->{'description'} = &$escape_function($line_item->{'description'});
+      if ( exists $line_item->{'ext_description'} ) {
+        @{$detail->{'ext_description'}} = map {
+          &$escape_function($_);
+        } @{$line_item->{'ext_description'}};
+      }
+      $detail->{'amount'} = ( $old_latex ? '' : $money_char).
+                            $line_item->{'amount'};
+      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+      push @detail_items, $detail;
+      push @buf, [ $detail->{'description'},
+                   $money_char. sprintf("%10.2f", $line_item->{'amount'}),
+                 ];
     }
-    $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-  
-    push @detail_items, $detail;
-    push @buf, [ $detail->{'description'},
-                 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-               ];
+
   }
-  
-  if (@pr_cust_bill) {
+
+  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
     push @buf, ['','-----------'];
     push @buf, [ 'Total Previous Balance',
                  $money_char. sprintf("%10.2f", $pr_total) ];
     push @buf, ['',''];
   }
 
-  foreach my $section (@sections) {
+  foreach my $section (@sections, @$late_sections) {
 
     $section->{'subtotal'} = $other_money_char.
                              sprintf('%.2f', $section->{'subtotal'})
@@ -1992,24 +2241,26 @@ sub print_generic {
 
     my %options = ();
     $options{'section'} = $section if $multisection;
+    $options{'format'} = $format;
+    $options{'escape_function'} = $escape_function;
+    $options{'format_function'} = sub { () } unless $unsquelched;
+    $options{'unsquelched'} = $unsquelched;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
         ext_description => [],
       };
       $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = 1;
+      $detail->{'quantity'} = $line_item->{'quantity'};
       $detail->{'section'} = $section;
       $detail->{'description'} = &$escape_function($line_item->{'description'});
       if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = map {
-          &$escape_function($_);
-        } @{$line_item->{'ext_description'}};
-      }
-      {
-        my $money = $old_latex ? '' : $money_char;
-        $detail->{'amount'} = $money. $line_item->{'amount'};
+        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
       }
+      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
+                              $line_item->{'amount'};
+      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
+                                 $line_item->{'unit_amount'};
       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
   
       push @detail_items, $detail;
@@ -2032,37 +2283,68 @@ sub print_generic {
   
   }
   
-  if ( $multisection ) {
-    unshift @sections, $previous_section;
+  if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+    unshift @sections, $previous_section if $pr_total;
   }
 
-  my $taxtotal = 0;
   foreach my $tax ( $self->_items_tax ) {
-    my $total = {};
-    $total->{'total_item'} = &$escape_function($tax->{'description'});
+
     $taxtotal += $tax->{'amount'};
-    $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
-    push @total_items, $total;
-    push @buf,[ $total->{'total_item'},
-                $money_char. sprintf("%10.2f", $total->{'total_amount'}),
+
+    my $description = &$escape_function( $tax->{'description'} );
+    my $amount      = sprintf( '%.2f', $tax->{'amount'} );
+
+    if ( $multisection ) {
+
+      my $money = $old_latex ? '' : $money_char;
+      push @detail_items, {
+        ext_description => [],
+        ref          => '',
+        quantity     => '',
+        description  => $description,
+        amount       => $money. $amount,
+        product_code => '',
+        section      => $tax_section,
+      };
+
+    } else {
+
+      push @total_items, {
+        'total_item'   => $description,
+        'total_amount' => $other_money_char. $amount,
+      };
+
+    }
+
+    push @buf,[ $description,
+                $money_char. $amount,
               ];
 
   }
   
   if ( $taxtotal ) {
     my $total = {};
+    $total->{'total_item'} = 'Sub-total';
+    $total->{'total_amount'} =
+      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
+
     if ( $multisection ) {
-      $total->{'total_item'} = 'New charges sub-total';
+      $tax_section->{'subtotal'} = $other_money_char.
+                                   sprintf('%.2f', $taxtotal);
+      $tax_section->{'pretotal'} = 'New charges sub-total '.
+                                   $total->{'total_amount'};
+      push @sections, $tax_section if $taxtotal;
     }else{
-      $total->{'total_item'} = 'Sub-total';
+      unshift @total_items, $total;
     }
-    $total->{'total_amount'} =
-      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-    unshift @total_items, $total;
   }
+  $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
   
   push @buf,['','-----------'];
-  push @buf,['Total New Charges',
+  push @buf,[( $conf->exists('disable_previous_balance') 
+               ? 'Total Charges'
+               : 'Total New Charges'
+             ),
              $money_char. sprintf("%10.2f",$self->charged) ];
   push @buf,['',''];
 
@@ -2070,71 +2352,127 @@ sub print_generic {
     my $total = {};
     $total->{'total_item'} = &$embolden_function('Total');
     $total->{'total_amount'} =
-    $total->{'total_amount'} =
       &$embolden_function(
-        $other_money_char.  sprintf('%.2f', $self->charged + $pr_total )
+        $other_money_char.
+        sprintf( '%.2f',
+                 $self->charged + ( $conf->exists('disable_previous_balance')
+                                    ? 0
+                                    : $pr_total
+                                  )
+               )
       );
-    push @total_items, $total;
+    if ( $multisection ) {
+      $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
+                                      sprintf('%.2f', $self->charged );
+    }else{
+      push @total_items, $total;
+    }
     push @buf,['','-----------'];
     push @buf,['Total Charges',
-               $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
+               $money_char.
+               sprintf( '%10.2f', $self->charged +
+                                    ( $conf->exists('disable_previous_balance')
+                                        ? 0
+                                        : $pr_total
+                                    )
+                      )
+              ];
     push @buf,['',''];
   }
   
+  unless ( $conf->exists('disable_previous_balance') ) {
+    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+  
+    # credits
+    my $credittotal = 0;
+    foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
+
+      my $total;
+      $total->{'total_item'} = &$escape_function($credit->{'description'});
+      $credittotal += $credit->{'amount'};
+      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+      $adjusttotal += $credit->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($credit->{'description'}),
+          amount       => $money. $credit->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      } else {
+        push @total_items, $total;
+      }
 
-  #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+    }
+    $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
+
+    #credits (again)
+    foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
+      push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
+    }
   
-  # credits
-  foreach my $credit ( $self->_items_credits ) {
-    my $total;
-    $total->{'total_item'} = &$escape_function($credit->{'description'});
-    #$credittotal
-    $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
-    push @total_items, $total;
-  }
+    # payments
+    my $paymenttotal = 0;
+    foreach my $payment ( $self->_items_payments ) {
+      my $total = {};
+      $total->{'total_item'} = &$escape_function($payment->{'description'});
+      $paymenttotal += $payment->{'amount'};
+      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+      $adjusttotal += $payment->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($payment->{'description'}),
+          amount       => $money. $payment->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      }else{
+        push @total_items, $total;
+      }
+      push @buf, [ $payment->{'description'},
+                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
+                 ];
+    }
+    $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
   
-  # credits (again)
-  foreach ( $self->cust_credited ) {
-
-    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+    if ( $multisection ) {
+      $adjust_section->{'subtotal'} = $other_money_char.
+                                      sprintf('%.2f', $adjusttotal);
+      push @sections, $adjust_section;
+    }
 
-    my $reason = substr($_->cust_credit->reason,0,32);
-    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
-    $reason = " ($reason) " if $reason;
-    push @buf,[
-      "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
-      $money_char. sprintf("%10.2f",$_->amount)
-    ];
+    { 
+      my $total;
+      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+      $total->{'total_amount'} =
+        &$embolden_function(
+          $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
+        );
+      if ( $multisection ) {
+        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                         $total->{'total_amount'};
+      }else{
+        push @total_items, $total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->balance_due_msg, $money_char. 
+        sprintf("%10.2f", $balance_due ) ];
+    }
   }
 
-  # payments
-  foreach my $payment ( $self->_items_payments ) {
-    my $total = {};
-    $total->{'total_item'} = &$escape_function($payment->{'description'});
-    #$paymenttotal
-    $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
-    push @total_items, $total;
-    push @buf, [ $payment->{'description'},
-                 $money_char. sprintf("%10.2f", $payment->{'amount'}),
-               ];
-  }
-  
-  { 
-    my $total;
-    $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-    $total->{'total_amount'} =
-      &$embolden_function(
-        $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
-      );
-    push @total_items, $total;
-    push @buf,['','-----------'];
-    push @buf,[$self->balance_due_msg, $money_char. 
-      sprintf("%10.2f", $balance_due ) ];
+  if ( $multisection ) {
+    push @sections, @$late_sections
+      if $unsquelched;
   }
 
-  $invoice_data{'logo_file'} = $params{'logo_file'}
-    if $params{'logo_file'};
-
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
@@ -2202,8 +2540,8 @@ sub print_ps {
   my ($file, $lfile) = $self->print_latex(@_);
   my $ps = generate_ps($file);
   unlink($lfile);
-  $ps;
 
+  $ps;
 }
 
 =item print_pdf [ TIME [ , TEMPLATE ] ]
@@ -2221,44 +2559,10 @@ sub print_pdf {
   my $self = shift;
 
   my ($file, $lfile) = $self->print_latex(@_);
+  my $pdf = generate_pdf($file);
+  unlink($lfile);
 
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  chdir($dir);
-
-  #system('pdflatex', "$file.tex");
-  #system('pdflatex', "$file.tex");
-  #! LaTeX Error: Unknown graphics extension: .eps.
-
-  my $sfile = shell_quote $file;
-
-  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed; see $file.log for details?\n";
-  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed; see $file.log for details?\n";
-
-  #system('dvipdf', "$file.dvi", "$file.pdf" );
-  system(
-    "dvips -q -t letter -f $sfile.dvi ".
-    "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
-    "     -c save pop -"
-  ) == 0
-    or die "dvips | gs failed: $!";
-
-  open(PDF, "<$file.pdf")
-    or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
-
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
-  unlink("$lfile");
-
-  my $pdf = '';
-  while (<PDF>) {
-    $pdf .= $_;
-  }
-
-  close PDF;
-
-  return $pdf;
-
+  $pdf;
 }
 
 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
@@ -2276,12 +2580,17 @@ when emailing the invoice as part of a multipart/related MIME email.
 =cut
 
 sub print_html {
-  my( $self, $today, $template, $cid ) = @_;
+  my $self = shift;
+  my %params;
+  if ( ref $_[0]  ) {
+    %params = %{ shift() }; 
+  }else{
+    $params{'time'} = shift;
+    $params{'template'} = shift;
+    $params{'cid'} = shift;
+  }
 
-  my %params = ( 'format' => 'html' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{'cid'} = $cid if $cid;
+  $params{'format'} = 'html';
 
   $self->print_generic( %params );
 }
@@ -2374,8 +2683,8 @@ sub terms {
   return $self->cust_main->invoice_terms
     if $self->cust_main->invoice_terms;
 
-  #use configured default or default default
-  $conf->config('invoice_default_terms') || 'Payable upon receipt';
+  #use configured default
+  $conf->config('invoice_default_terms') || '';
 }
 
 sub due_date {
@@ -2404,37 +2713,123 @@ sub balance_due_msg {
   $msg;
 }
 
+sub balance_due_date {
+  my $self = shift;
+  my $duedate = '';
+  if (    $conf->exists('invoice_default_terms') 
+       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
+    $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+  }
+  $duedate;
+}
+
+=item invnum_date_pretty
+
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)"
+
+=cut
+
+sub invnum_date_pretty {
+  my $self = shift;
+  'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
+}
+
+=item _date_pretty
+
+Returns a string with the date, for example: "3/20/2008"
+
+=cut
+
+sub _date_pretty {
+  my $self = shift;
+  time2str('%x', $self->_date);
+}
+
 sub _items_sections {
   my $self = shift;
+  my $late = shift;
 
   my %s = ();
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+  my %l = ();
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
-
-      my $desc = $cust_bill_pkg->cust_pkg->part_pkg->classname;
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+  {
 
-      $s{$desc} += $cust_bill_pkg->setup
-        if ( $cust_bill_pkg->setup != 0 );
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
+      my $usage = $cust_bill_pkg->usage;
+
+      foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
+        my $desc = $display->section;
+        my $type = $display->type;
+
+        if ( $display->post_total ) {
+          if (! $type || $type eq 'S') {
+            $l{$desc} += $cust_bill_pkg->setup
+              if ( $cust_bill_pkg->setup != 0 );
+          }
+
+          if (! $type) {
+            $l{$desc} += $cust_bill_pkg->recur
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+
+          if ($type && $type eq 'R') {
+            $l{$desc} += $cust_bill_pkg->recur - $usage
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+          
+          if ($type && $type eq 'U') {
+            $l{$desc} += $usage;
+          }
+
+        } else {
+          if (! $type || $type eq 'S') {
+            $s{$desc} += $cust_bill_pkg->setup
+              if ( $cust_bill_pkg->setup != 0 );
+          }
+
+          if (! $type) {
+            $s{$desc} += $cust_bill_pkg->recur
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+
+          if ($type && $type eq 'R') {
+            $s{$desc} += $cust_bill_pkg->recur - $usage
+              if ( $cust_bill_pkg->recur != 0 );
+          }
+          
+          if ($type && $type eq 'U') {
+            $s{$desc} += $usage;
+          }
+
+        }
 
-      $s{$desc} += $cust_bill_pkg->recur
-        if ( $cust_bill_pkg->recur != 0 );
+      }
 
     }
 
   }
 
+  push @$late, map { { 'description' => $_,
+                       'subtotal'    => $l{$_},
+                       'post_total'  => 1,
+                   } } sort keys %l;
+
   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
 
 }
 
 sub _items {
   my $self = shift;
-  my @display = scalar(@_)
-                ? @_
-                : qw( _items_previous _items_pkg );
-                #: qw( _items_pkg );
-                #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+
+  #my @display = scalar(@_)
+  #              ? @_
+  #              : qw( _items_previous _items_pkg );
+  #              #: qw( _items_pkg );
+  #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+  my @display = qw( _items_previous _items_pkg );
+
   my @b = ();
   foreach my $display ( @display ) {
     push @b, $self->$display(@_);
@@ -2474,16 +2869,8 @@ sub _items_previous {
 
 sub _items_pkg {
   my $self = shift;
-  my %options = @_;
-  my $section = delete $options{'section'};
-  my @cust_bill_pkg =
-    grep { $_->pkgnum &&
-           ( defined($section)
-               ? $_->cust_pkg->part_pkg->classname eq $section->{'description'}
-               : 1
-           )
-         } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
+  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
 }
 
 sub _taxsort {
@@ -2504,79 +2891,202 @@ sub _items_tax {
 sub _items_cust_bill_pkg {
   my $self = shift;
   my $cust_bill_pkg = shift;
+  my %opt = @_;
 
-  my @b = ();
-  foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
-
-    my $cust_pkg = $cust_bill_pkg->cust_pkg;
-
-    my $desc = $cust_bill_pkg->desc;
+  my $format = $opt{format} || '';
+  my $escape_function = $opt{escape_function} || sub { shift };
+  my $format_function = $opt{format_function} || '';
+  my $unsquelched = $opt{unsquelched} || '';
+  my $section = $opt{section}->{description} if $opt{section};
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+  my @b = ();
+  my ($s, $r, $u) = ( undef, undef, undef );
+  foreach my $cust_bill_pkg ( @$cust_bill_pkg )
+  {
 
-      if ( $cust_bill_pkg->setup != 0 ) {
-        my $description = $desc;
-        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = $cust_pkg->h_labels_short($self->_date);
-        push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
-        push @b, {
-          description     => $description,
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          amount          => sprintf("%.2f", $cust_bill_pkg->setup),
-          ext_description => \@d,
-        };
+    foreach ( $s, $r, $u ) {
+      if ( $_ && !$cust_bill_pkg->hidden ) {
+        $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+        $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+        push @b, { %$_ };
+        $_ = undef;
       }
+    }
 
-      if ( $cust_bill_pkg->recur != 0 ) {
-        push @b, {
-          description     => $desc .
-                             ( $conf->exists('disable_line_item_date_ranges')
-                               ? ''
-                               : " (" .time2str("%x", $cust_bill_pkg->sdate).
-                                 " - ".time2str("%x", $cust_bill_pkg->edate).")"
-                             ),
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          amount          => sprintf("%.2f", $cust_bill_pkg->recur),
-          ext_description =>
-            #at least until cust_bill_pkg has "past" ranges in addition to
-            #the "future" sdate/edate ones... see #3032
-            [ $cust_pkg->h_labels_short( $self->_date ),
-                                         #$cust_bill_pkg->edate,
-                                         #$cust_bill_pkg->sdate),
-              $cust_bill_pkg->details,
-            ],
-        };
-      }
+    foreach my $display ( grep { defined($section)
+                                 ? $_->section eq $section
+                                 : 1
+                               }
+                          $cust_bill_pkg->cust_bill_pkg_display
+                        )
+    {
 
-    } else { #pkgnum tax or one-shot line item (??)
+      my $type = $display->type;
+
+      my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
+      my $desc = $cust_bill_pkg->desc;
+      $desc = substr($desc, 0, 50). '...'
+        if $format eq 'latex' && length($desc) > 50;
+
+      my %details_opt = ( 'format'          => $format,
+                          'escape_function' => $escape_function,
+                          'format_function' => $format_function,
+                        );
+
+      if ( $cust_bill_pkg->pkgnum > 0 ) {
+
+        if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
+
+          my $description = $desc;
+          $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+
+          my @d = ();
+          push @d, map &{$escape_function}($_),
+                       $cust_pkg->h_labels_short($self->_date)
+            unless $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->hidden;
+          push @d, $cust_bill_pkg->details(%details_opt)
+            if $cust_bill_pkg->recur == 0;
+
+          if ( $cust_bill_pkg->hidden ) {
+            $s->{amount}      += $cust_bill_pkg->setup;
+            $s->{unit_amount} += $cust_bill_pkg->unitsetup;
+            push @{ $s->{ext_description} }, @d;
+          } else {
+            $s = {
+              description     => $description,
+              #pkgpart         => $part_pkg->pkgpart,
+              pkgnum          => $cust_bill_pkg->pkgnum,
+              amount          => $cust_bill_pkg->setup,
+              unit_amount     => $cust_bill_pkg->unitsetup,
+              quantity        => $cust_bill_pkg->quantity,
+              ext_description => \@d,
+            };
+          };
+
+        }
+
+        if ( $cust_bill_pkg->recur != 0 &&
+             ( !$type || $type eq 'R' || $type eq 'U' )
+           )
+        {
+
+          my $is_summary = $display->summary;
+          my $description = $is_summary ? "Usage charges" : $desc;
+
+          unless ( $conf->exists('disable_line_item_date_ranges') ) {
+            $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
+                            " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+          }
+
+          my @d = ();
+
+          #at least until cust_bill_pkg has "past" ranges in addition to
+          #the "future" sdate/edate ones... see #3032
+          my @dates = ( $self->_date );
+          my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
+          push @dates, $prev->sdate if $prev;
+
+          push @d, map &{$escape_function}($_),
+                       $cust_pkg->h_labels_short(@dates)
+                                                 #$cust_bill_pkg->edate,
+                                                 #$cust_bill_pkg->sdate)
+            unless $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->itemdesc
+                || $cust_bill_pkg->hidden
+                || $is_summary;
+
+          push @d, $cust_bill_pkg->details(%details_opt)
+            unless ($is_summary || $type && $type eq 'R');
+  
+          my $amount = 0;
+          if (!$type) {
+            $amount = $cust_bill_pkg->recur;
+          }elsif($type eq 'R') {
+            $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+          }elsif($type eq 'U') {
+            $amount = $cust_bill_pkg->usage;
+          }
+  
+          if ( !$type || $type eq 'R' ) {
+
+            if ( $cust_bill_pkg->hidden ) {
+              $r->{amount}      += $amount;
+              $r->{unit_amount} += $cust_bill_pkg->unitrecur;
+              push @{ $r->{ext_description} }, @d;
+            } else {
+              $r = {
+                description     => $description,
+                #pkgpart         => $part_pkg->pkgpart,
+                pkgnum          => $cust_bill_pkg->pkgnum,
+                amount          => $amount,
+                unit_amount     => $cust_bill_pkg->unitrecur,
+                quantity        => $cust_bill_pkg->quantity,
+                ext_description => \@d,
+              };
+            }
+
+          } elsif ( $amount ) {  # && $type eq 'U'
+
+            if ( $cust_bill_pkg->hidden ) {
+              $u->{amount}      += $amount;
+              $u->{unit_amount} += $cust_bill_pkg->unitrecur;
+              push @{ $u->{ext_description} }, @d;
+            } else {
+              $u = {
+                description     => $description,
+                #pkgpart         => $part_pkg->pkgpart,
+                pkgnum          => $cust_bill_pkg->pkgnum,
+                amount          => $amount,
+                unit_amount     => $cust_bill_pkg->unitrecur,
+                quantity        => $cust_bill_pkg->quantity,
+                ext_description => \@d,
+              };
+            }
+
+          }
+
+        } # recurring or usage with recurring charge
+
+      } else { #pkgnum tax or one-shot line item (??)
+
+        if ( $cust_bill_pkg->setup != 0 ) {
+          push @b, {
+            'description' => $desc,
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
+          };
+        }
+        if ( $cust_bill_pkg->recur != 0 ) {
+          push @b, {
+            'description' => "$desc (".
+                             time2str("%x", $cust_bill_pkg->sdate). ' - '.
+                             time2str("%x", $cust_bill_pkg->edate). ')',
+            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
+          };
+        }
 
-      if ( $cust_bill_pkg->setup != 0 ) {
-        push @b, {
-          'description' => $desc,
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-        };
-      }
-      if ( $cust_bill_pkg->recur != 0 ) {
-        push @b, {
-          'description' => "$desc (".
-                           time2str("%x", $cust_bill_pkg->sdate). ' - '.
-                           time2str("%x", $cust_bill_pkg->edate). ')',
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-        };
       }
 
     }
 
   }
 
+  foreach ( $s, $r, $u ) {
+    if ( $_ ) {
+      $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+      $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+      push @b, { %$_ };
+    }
+  }
+
   @b;
 
 }
 
 sub _items_credits {
-  my $self = shift;
+  my( $self, %opt ) = @_;
+  my $trim_len = $opt{'trim_len'} || 60;
 
   my @b;
   #credits
@@ -2584,10 +3094,10 @@ sub _items_credits {
 
     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
 
-    my $reason = $_->cust_credit->reason;
-    #my $reason = substr($_->cust_credit->reason,0,32);
-    #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
+    my $reason = substr($_->cust_credit->reason, 0, $trim_len);
+    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
     $reason = " ($reason) " if $reason;
+
     push @b, {
       #'description' => 'Credit ref\#'. $_->crednum.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
@@ -2597,12 +3107,6 @@ sub _items_credits {
       'amount'      => sprintf("%.2f",$_->amount),
     };
   }
-  #foreach ( @cr_cust_credit ) {
-  #  push @buf,[
-  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
-  #    $money_char. sprintf("%10.2f",$_->credited)
-  #  ];
-  #}
 
   @b;
 
@@ -2628,6 +3132,38 @@ sub _items_payments {
 
 }
 
+=item call_details [ OPTION => VALUE ... ]
+
+Returns an array of CSV strings representing the call details for this invoice
+The only option available is the boolean prepend_billed_number
+
+=cut
+
+sub call_details {
+  my ($self, %opt) = @_;
+
+  my $format_function = sub { shift };
+
+  if ($opt{prepend_billed_number}) {
+    $format_function = sub {
+      my $detail = shift;
+      my $row = shift;
+
+      $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
+      
+    };
+  }
+
+  my @details = map { $_->details( 'format_function' => $format_function,
+                                   'escape_function' => sub{ return() },
+                                 )
+                    }
+                  grep { $_->pkgnum }
+                  $self->cust_bill_pkg;
+  my $header = $details[0];
+  ( $header, grep { $_ ne $header } @details );
+}
+
 
 =back
 
@@ -2635,7 +3171,7 @@ sub _items_payments {
 
 =over 4
 
-=item reprint
+=item process_reprint
 
 =cut
 
@@ -2643,7 +3179,7 @@ sub process_reprint {
   process_re_X('print', @_);
 }
 
-=item reemail
+=item process_reemail
 
 =cut
 
@@ -2651,7 +3187,7 @@ sub process_reemail {
   process_re_X('email', @_);
 }
 
-=item refax
+=item process_refax
 
 =cut
 
@@ -2659,6 +3195,22 @@ sub process_refax {
   process_re_X('fax', @_);
 }
 
+=item process_reftp
+
+=cut
+
+sub process_reftp {
+  process_re_X('ftp', @_);
+}
+
+=item respool
+
+=cut
+
+sub process_respool {
+  process_re_X('spool', @_);
+}
+
 use Storable qw(thaw);
 use Data::Dumper;
 use MIME::Base64;
@@ -2702,6 +3254,8 @@ sub re_X {
     'debug' => 1,
   } );
 
+  $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
+
   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
     if $DEBUG;