DID activity summary improvements, RT10886
[freeside.git] / FS / FS / cust_bill.pm
index 55faa36..9d250eb 100644 (file)
@@ -1,9 +1,11 @@
 package FS::cust_bill;
 
 use strict;
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
+use vars qw( @ISA $DEBUG $me $conf
+             $money_char $date_format $rdate_format $date_format_long );
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
+use Cwd;
 use List::Util qw(min max);
 use Date::Format;
 use Text::Template 1.20;
 use List::Util qw(min max);
 use Date::Format;
 use Text::Template 1.20;
@@ -11,6 +13,8 @@ use File::Temp 0.14;
 use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
 use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
+use Storable qw( freeze thaw );
+use GD::Barcode;
 use FS::UID qw( datasrc );
 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::UID qw( datasrc );
 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
 use FS::Record qw( qsearch qsearchs dbh );
@@ -19,6 +23,7 @@ use FS::cust_main;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_detail;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit;
 use FS::cust_pay;
 use FS::cust_pkg;
@@ -32,6 +37,10 @@ use FS::cust_bill_pay;
 use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
 use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
+use FS::bill_batch;
+use FS::cust_bill_batch;
+use FS::cust_bill_pay_pkg;
+use FS::cust_credit_bill_pkg;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
@@ -41,7 +50,10 @@ $me = '[FS::cust_bill]';
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
-  $money_char = $conf->config('money_char') || '$';  
+  $money_char       = $conf->config('money_char')       || '$';  
+  $date_format      = $conf->config('date_format')      || '%x'; #/YY
+  $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
+  $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
 } );
 
 =head1 NAME
 } );
 
 =head1 NAME
@@ -96,6 +108,18 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item charged - amount of this invoice
 
 
 =item charged - amount of this invoice
 
+=item invoice_terms - optional terms override for this specific invoice
+
+=back
+
+Customer info at invoice generation time
+
+=over 4
+
+=item previous_balance
+
+=item billing_balance
+
 =back
 
 Deprecated
 =back
 
 Deprecated
@@ -144,6 +168,45 @@ sub cust_unlinked_msg {
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
 returns the error, otherwise returns false.
 
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
 returns the error, otherwise returns false.
 
+=cut
+
+sub insert {
+  my $self = shift;
+  warn "$me insert called\n" if $DEBUG;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->get('cust_bill_pkg') ) {
+    foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
+      $cust_bill_pkg->invnum($self->invnum);
+      my $error = $cust_bill_pkg->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "can't create invoice line item: $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item delete
 
 This method now works but you probably shouldn't use it.  Instead, apply a
 =item delete
 
 This method now works but you probably shouldn't use it.  Instead, apply a
@@ -182,6 +245,7 @@ sub delete {
     cust_pay_batch
     cust_bill_pay_batch
     cust_bill_pkg
     cust_pay_batch
     cust_bill_pay_batch
     cust_bill_pkg
+    cust_bill_batch
   )) {
 
     foreach my $linked ( $self->$table() ) {
   )) {
 
     foreach my $linked ( $self->$table() ) {
@@ -206,13 +270,13 @@ sub delete {
 
 }
 
 
 }
 
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ]
 
 
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
+You can, but probably shouldn't modify invoices...
 
 
-Only printed may be changed.  printed is normally updated by calling the
-collect method of a customer object (see L<FS::cust_main>).
+Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
+supplied, replaces this record.  If there is an error, returns the error,
+otherwise returns false.
 
 =cut
 
 
 =cut
 
@@ -223,15 +287,44 @@ collect method of a customer object (see L<FS::cust_main>).
 
 sub replace_check {
   my( $new, $old ) = ( shift, shift );
 
 sub replace_check {
   my( $new, $old ) = ( shift, shift );
-  return "Can't change custnum!" unless $old->custnum == $new->custnum;
+  return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   #return "Can't change _date!" unless $old->_date eq $new->_date;
-  return "Can't change _date!" unless $old->_date == $new->_date;
-  return "Can't change charged!" unless $old->charged == $new->charged
-                                     || $old->charged == 0;
+  return "Can't change _date" unless $old->_date == $new->_date;
+  return "Can't change charged" unless $old->charged == $new->charged
+                                    || $old->charged == 0
+                                   || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
   '';
 }
 
 
   '';
 }
 
+
+=item add_cc_surcharge
+
+Giant hack
+
+=cut
+
+sub add_cc_surcharge {
+    my ($self, $pkgnum, $amount) = (shift, shift, shift);
+
+    my $error;
+    my $cust_bill_pkg = new FS::cust_bill_pkg({
+                                   'invnum' => $self->invnum,
+                                   'pkgnum' => $pkgnum,
+                                   'setup' => $amount,
+                       });
+    $error = $cust_bill_pkg->insert;
+    return $error if $error;
+
+    $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
+    $self->charged($self->charged+$amount);
+    $error = $self->replace;
+    return $error if $error;
+
+    $self->apply_payments_and_credits;
+}
+
+
 =item check
 
 Checks all fields to make sure this is a valid invoice.  If there is an error,
 =item check
 
 Checks all fields to make sure this is a valid invoice.  If there is an error,
@@ -340,11 +433,24 @@ this invoice.
 
 sub cust_pkg {
   my $self = shift;
 
 sub cust_pkg {
   my $self = shift;
-  my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+  my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+                     $self->cust_bill_pkg;
   my %saw = ();
   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
 }
 
   my %saw = ();
   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
 }
 
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+  my $self = shift;
+  grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
 =item open_cust_bill_pkg
 
 Returns the open line items for this invoice.
 =item open_cust_bill_pkg
 
 Returns the open line items for this invoice.
@@ -521,6 +627,7 @@ Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
 
 sub cust_bill_pay {
   my $self = shift;
 
 sub cust_bill_pay {
   my $self = shift;
+  map { $_ } #return $self->num_cust_bill_pay unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
 }
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
 }
@@ -535,6 +642,7 @@ Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
 
 sub cust_credited {
   my $self = shift;
 
 sub cust_credited {
   my $self = shift;
+  map { $_ } #return $self->num_cust_credit_bill unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
   ;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
   ;
@@ -544,36 +652,97 @@ sub cust_credit_bill {
   shift->cust_credited(@_);
 }
 
   shift->cust_credited(@_);
 }
 
-=item cust_bill_pay_pkgnum PKGNUM
+#=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 ) = @_;
+#  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+#                                'pkgnum' => $pkgnum,
+#                              }
+#           );
+#}
+
+=item cust_bill_pay_pkg PKGNUM
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
-with matching pkgnum.
+applied against the matching pkgnum.
 
 =cut
 
 
 =cut
 
-sub cust_bill_pay_pkgnum {
+sub cust_bill_pay_pkg {
   my( $self, $pkgnum ) = @_;
   my( $self, $pkgnum ) = @_;
-  sort { $a->_date <=> $b->_date }
-    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
-                                'pkgnum' => $pkgnum,
-                              }
-           );
+
+  qsearch({
+    'select'    => 'cust_bill_pay_pkg.*',
+    'table'     => 'cust_bill_pay_pkg',
+    'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
+                   ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
+    'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+                   "   AND cust_bill_pkg.pkgnum = $pkgnum",
+  });
+
 }
 
 }
 
-=item cust_credited_pkgnum PKGNUM
+#=item cust_credited_pkgnum PKGNUM
+#
+#=item cust_credit_bill_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 ) = @_;
+#  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+#                                   'pkgnum' => $pkgnum,
+#                                 }
+#           );
+#}
+#
+#sub cust_credit_bill_pkgnum {
+#  shift->cust_credited_pkgnum(@_);
+#}
+
+=item cust_credit_bill_pkg PKGNUM
 
 
-Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
-with matching pkgnum.
+Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
+applied against the matching pkgnum.
 
 =cut
 
 
 =cut
 
-sub cust_credited_pkgnum {
+sub cust_credit_bill_pkg {
   my( $self, $pkgnum ) = @_;
   my( $self, $pkgnum ) = @_;
-  sort { $a->_date <=> $b->_date }
-    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
-                                   'pkgnum' => $pkgnum,
-                                 }
-           );
+
+  qsearch({
+    'select'    => 'cust_credit_bill_pkg.*',
+    'table'     => 'cust_credit_bill_pkg',
+    'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
+                   ' LEFT JOIN cust_bill_pkg    USING ( billpkgnum    ) ',
+    'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+                   "   AND cust_bill_pkg.pkgnum = $pkgnum",
+  });
+
+}
+
+=item cust_bill_batch
+
+Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
+
+=cut
+
+sub cust_bill_batch {
+  my $self = shift;
+  qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
 }
 
 =item tax
 }
 
 =item tax
@@ -616,8 +785,8 @@ sub owed_pkgnum {
   my $balance = 0;
   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
 
   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 -= $_->amount            for $self->cust_bill_pay_pkg($pkgnum);
+  $balance -= $_->amount            for $self->cust_credit_bill_pkg($pkgnum);
 
   $balance = sprintf( "%.2f", $balance);
   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
 
   $balance = sprintf( "%.2f", $balance);
   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
@@ -770,6 +939,10 @@ text attachment arrayref, optional
 
 email subject, optional
 
 
 email subject, optional
 
+=item notice_name
+
+notice name instead of "Invoice", optional
+
 =back
 
 Returns an argument list to be passed to L<FS::Misc::send_email>.
 =back
 
 Returns an argument list to be passed to L<FS::Misc::send_email>.
@@ -790,13 +963,20 @@ sub generate_email {
     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
   );
 
     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
   );
 
-  my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
+  my %opt = (
+    'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+    'template'      => $args{'template'},
+    'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
+    'no_coupon'     => $args{'no_coupon'},
+  );
+
+  my $cust_main = $self->cust_main;
 
   if (ref($args{'to'}) eq 'ARRAY') {
     $return{'to'} = $args{'to'};
   } else {
     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
 
   if (ref($args{'to'}) eq 'ARRAY') {
     $return{'to'} = $args{'to'};
   } else {
     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
-                           $self->cust_main->invoicing_list
+                           $cust_main->invoicing_list
                     ];
   }
 
                     ];
   }
 
@@ -830,7 +1010,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
-        $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
+        $data = [ $self->print_text(\%opt) ];
       }
 
     }
       }
 
     }
@@ -848,7 +1028,7 @@ sub generate_email {
     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
 
     my $logo;
     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
 
     my $logo;
-    my $agentnum = $self->cust_main->agentnum;
+    my $agentnum = $cust_main->agentnum;
     if ( defined($args{'template'}) && length($args{'template'})
          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
        )
     if ( defined($args{'template'}) && length($args{'template'})
          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
        )
@@ -866,6 +1046,19 @@ sub generate_email {
       'Filename'   => 'logo.png',
       'Content-ID' => "<$content_id>",
     ;
       'Filename'   => 'logo.png',
       'Content-ID' => "<$content_id>",
     ;
+   
+    my $barcode;
+    if($conf->exists('invoice-barcode')){
+       my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
+       $barcode = build MIME::Entity
+         'Type'       => 'image/png',
+         'Encoding'   => 'base64',
+         'Data'       => $self->invoice_barcode(0),
+         'Filename'   => 'barcode.png',
+         'Content-ID' => "<$barcode_content_id>",
+       ;
+       $opt{'barcode_cid'} = $barcode_content_id;
+    }
 
     $alternative->attach(
       'Type'        => 'text/html',
 
     $alternative->attach(
       'Type'        => 'text/html',
@@ -877,11 +1070,7 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $self->print_html({ time          => '',
-                                             template      => $args{'template'},
-                                             cid           => $content_id,
-                                             %cdrs,
-                                          }),
+                         $self->print_html({ 'cid'=>$content_id, %opt }),
                          '  </body>',
                          '</html>',
                        ],
                          '  </body>',
                          '</html>',
                        ],
@@ -890,7 +1079,7 @@ sub generate_email {
     );
 
     my @otherparts = ();
     );
 
     my @otherparts = ();
-    if ( $self->cust_main->email_csv_cdr ) {
+    if ( $cust_main->email_csv_cdr ) {
 
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
 
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
@@ -929,7 +1118,7 @@ sub generate_email {
 
       $related->add_part($image);
 
 
       $related->add_part($image);
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
+      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
 
       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
 
       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
@@ -943,7 +1132,12 @@ sub generate_email {
       #   image/png
 
       $return{'content-type'} = 'multipart/related';
       #   image/png
 
       $return{'content-type'} = 'multipart/related';
-      $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
+      if($conf->exists('invoice-barcode')){
+         $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
+      }
+      else {
+         $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
+      }
       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
       #$return{'disposition'} = 'inline';
 
       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
       #$return{'disposition'} = 'inline';
 
@@ -957,7 +1151,7 @@ sub generate_email {
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
-        { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
+        { $self->mimebuild_pdf(\%opt) }
       ];
     }
   
       ];
     }
   
@@ -977,7 +1171,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
-        $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
+        $return{'body'} = [ $self->print_text(\%opt) ];
       }
 
     }
       }
 
     }
@@ -1006,22 +1200,27 @@ sub mimebuild_pdf {
   );
 }
 
   );
 }
 
-=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
 
 Sends this invoice to the destinations configured for this customer: sends
 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
 
 Sends this invoice to the destinations configured for this customer: sends
 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to 
+four values for templatename, agentnum, invoice_from and amount.
 
 
-AGENTNUM, if specified, means that this invoice will only be sent for customers
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<agentnum>, if specified, means that this invoice will only be sent for customers
 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
 single agent) or an arrayref of agentnums.
 
 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
 single agent) or an arrayref of agentnums.
 
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<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
+I<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.
 
 invoice and all older invoices is greater than the specified amount.
 
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub queueable_send {
 =cut
 
 sub queueable_send {
@@ -1041,48 +1240,73 @@ sub queueable_send {
 
 sub send {
   my $self = shift;
 
 sub send {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  if ( scalar(@_) && $_[0]  ) {
-    my $agentnums = ref($_[0]) ? shift : [ shift ];
-    return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
-  }
 
 
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from ||    #XXX should go away
-          $conf->config('invoice_from', $self->cust_main->agentnum )
-        );
+  my( $template, $invoice_from, $notice_name );
+  my $agentnums = '';
+  my $balance_over = 0;
+
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    if ( $agentnums = $opt->{'agentnum'} ) {
+      $agentnums = [ $agentnums ] unless ref($agentnums);
+    }
+    $invoice_from = $opt->{'invoice_from'};
+    $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
+    $notice_name = $opt->{'notice_name'};
+  } else {
+    $template = scalar(@_) ? shift : '';
+    if ( scalar(@_) && $_[0]  ) {
+      $agentnums = ref($_[0]) ? shift : [ shift ];
+    }
+    $invoice_from = shift if scalar(@_);
+    $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
+  }
 
 
-  my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+  return 'N/A' unless ! $agentnums
+                   or grep { $_ == $self->cust_main->agentnum } @$agentnums;
 
   return ''
     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
 
 
   return ''
     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
 
+  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
+                    $conf->config('invoice_from', $self->cust_main->agentnum );
+
+  my %opt = (
+    'template'     => $template,
+    'invoice_from' => $invoice_from,
+    'notice_name'  => ( $notice_name || 'Invoice' ),
+  );
+
   my @invoicing_list = $self->cust_main->invoicing_list;
 
   my @invoicing_list = $self->cust_main->invoicing_list;
 
-  #$self->email_invoice($template, $invoice_from)
-  $self->email($template, $invoice_from)
+  #$self->email_invoice(\%opt)
+  $self->email(\%opt)
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
-  #$self->print_invoice($template)
-  $self->print($template)
+  #$self->print_invoice(\%opt)
+  $self->print(\%opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-  $self->fax_invoice($template)
+  $self->fax_invoice(\%opt)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
 
 }
 
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
 
 }
 
-=item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
+=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
 
 Emails this invoice.
 
 
 Emails this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to 
+two values for templatename and invoice_from.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
 
 
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From: address.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 
 =cut
 
@@ -1092,11 +1316,12 @@ sub queueable_email {
   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
     or die "invalid invoice number: " . $opt{invnum};
 
   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
     or die "invalid invoice number: " . $opt{invnum};
 
-  my @args = ( $opt{template} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
+  my %args = ( 'template' => $opt{template} );
+  $args{$_} = $opt{$_}
+    foreach grep { exists($opt{$_}) && $opt{$_} }
+              qw( invoice_from notice_name no_coupon );
 
 
-  my $error = $self->email( @args );
+  my $error = $self->email( \%args );
   die $error if $error;
 
 }
   die $error if $error;
 
 }
@@ -1104,29 +1329,46 @@ sub queueable_email {
 #sub email_invoice {
 sub email {
   my $self = shift;
 #sub email_invoice {
 sub email {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from ||    #XXX should go away
-          $conf->config('invoice_from', $self->cust_main->agentnum )
-        );
 
 
+  my( $template, $invoice_from, $notice_name, $no_coupon );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $invoice_from = $opt->{'invoice_from'};
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+    $no_coupon = $opt->{'no_coupon'} || 0;
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $invoice_from = shift if scalar(@_);
+    $notice_name = 'Invoice';
+    $no_coupon = 0;
+  }
+
+  $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;
 
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
 
-  #better to notify this person than silence
-  @invoicing_list = ($invoice_from) unless @invoicing_list;
+  if ( ! @invoicing_list ) { #no recipients
+    if ( $conf->exists('cust_bill-no_recipients-error') ) {
+      die 'No recipients for customer #'. $self->custnum;
+    } else {
+      #default: better to notify this person than silence
+      @invoicing_list = ($invoice_from);
+    }
+  }
 
   my $subject = $self->email_subject($template);
 
   my $error = send_email(
     $self->generate_email(
 
   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,
+      'from'        => $invoice_from,
+      'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+      'subject'     => $subject,
+      'template'    => $template,
+      'notice_name' => $notice_name,
+      'no_coupon'   => $no_coupon,
     )
   );
   die "can't email invoice: $error\n" if $error;
     )
   );
   die "can't email invoice: $error\n" if $error;
@@ -1152,48 +1394,104 @@ sub email_subject {
   eval qq("$subject");
 }
 
   eval qq("$subject");
 }
 
-=item lpr_data [ TEMPLATENAME ]
+=item lpr_data HASHREF | [ TEMPLATE ]
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional value
+for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 sub lpr_data {
 
 =cut
 
 sub lpr_data {
-  my( $self, $template) = @_;
-  $conf->exists('invoice_latex')
-    ? [ $self->print_ps('', $template) ]
-    : [ $self->print_text('', $template) ];
+  my $self = shift;
+  my( $template, $notice_name );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $notice_name = 'Invoice';
+  }
+
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
+  [ $self->$method( \%opt ) ];
 }
 
 }
 
-=item print [ TEMPLATENAME ]
+=item print HASHREF | [ TEMPLATE ]
 
 Prints this invoice.
 
 
 Prints this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 #sub print_invoice {
 sub print {
   my $self = shift;
 
 =cut
 
 #sub print_invoice {
 sub print {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
+  my( $template, $notice_name );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $notice_name = 'Invoice';
+  }
 
 
-  do_print $self->lpr_data($template);
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  if($conf->exists('invoice_print_pdf')) {
+    # Add the invoice to the current batch.
+    $self->batch_invoice(\%opt);
+  }
+  else {
+    do_print $self->lpr_data(\%opt);
+  }
 }
 
 }
 
-=item fax_invoice [ TEMPLATENAME ] 
+=item fax_invoice HASHREF | [ TEMPLATE ] 
 
 Faxes this invoice.
 
 
 Faxes this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 sub fax_invoice {
   my $self = shift;
 
 =cut
 
 sub fax_invoice {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
+  my( $template, $notice_name );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $notice_name = 'Invoice';
+  }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
     unless $conf->exists('invoice_latex');
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
     unless $conf->exists('invoice_latex');
@@ -1201,13 +1499,35 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my $error = send_fax( 'docdata'    => $self->lpr_data($template),
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
 
 }
 
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
 
 }
 
+=item batch_invoice [ HASHREF ]
+
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+isn't an open batch, one will be created.
+
+=cut
+
+sub batch_invoice {
+  my ($self, $opt) = @_;
+  my $batch = FS::bill_batch->get_open_batch;
+  my $cust_bill_batch = FS::cust_bill_batch->new({
+      batchnum => $batch->batchnum,
+      invnum   => $self->invnum,
+  });
+  return $cust_bill_batch->insert($opt);
+}
+
 =item ftp_invoice [ TEMPLATENAME ] 
 
 Sends this invoice data via FTP.
 =item ftp_invoice [ TEMPLATENAME ] 
 
 Sends this invoice data via FTP.
@@ -1748,7 +2068,8 @@ sub realtime_lec {
 }
 
 sub realtime_bop {
 }
 
 sub realtime_bop {
-  my( $self, $method ) = @_;
+  my( $self, $method ) = (shift,shift);
+  my %opt = @_;
 
   my $cust_main = $self->cust_main;
   my $balance = $cust_main->balance;
 
   my $cust_main = $self->cust_main;
   my $balance = $cust_main->balance;
@@ -1774,6 +2095,15 @@ sub realtime_bop {
   $cust_main->realtime_bop($method, $amount,
     'description' => $description,
     'invnum'      => $self->invnum,
   $cust_main->realtime_bop($method, $amount,
     'description' => $description,
     'invnum'      => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+#    'apply'       => 1,
+    'apply_to_invoice' => 1,
+    %opt,
+ #what we want:
+ #this changes application behavior: auto payments
+                        #triggered against a specific invoice are now applied
+                        #to that invoice instead of oldest open.
+                        #seem okay to me...
   );
 
 }
   );
 
 }
@@ -1805,29 +2135,45 @@ sub _agent_invoice_from {
   $self->cust_main->agent_invoice_from;
 }
 
   $self->cust_main->agent_invoice_from;
 }
 
-=item print_text [ TIME [ , TEMPLATE ] ]
+=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
 
 Returns an text invoice, as a list of lines.
 
 
 Returns an text invoice, as a list of lines.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_text {
 =cut
 
 sub print_text {
-  my( $self, $today, $template, %opt ) = @_;
+  my $self = shift;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
 
   my %params = ( 'format' => 'template' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
 
   my %params = ( 'format' => 'template' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
-  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
 
   $self->print_generic( %params );
 }
 
 
   $self->print_generic( %params );
 }
 
-=item print_latex [ TIME [ , TEMPLATE ] ]
+=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
 
 Internal method - returns a filename of a filled-in LaTeX template for this
 invoice (Note: add ".tex" to get the actual filename), and a filename of
 
 Internal method - returns a filename of a filled-in LaTeX template for this
 invoice (Note: add ".tex" to get the actual filename), and a filename of
@@ -1835,20 +2181,36 @@ an associated logo (with the .eps extension included).
 
 See print_ps and print_pdf for methods that return PostScript and PDF output.
 
 
 See print_ps and print_pdf for methods that return PostScript and PDF output.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_latex {
 =cut
 
 sub print_latex {
-  my( $self, $today, $template, %opt ) = @_;
+  my $self = shift;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
 
   my %params = ( 'format' => 'latex' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
 
   my %params = ( 'format' => 'latex' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
-  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
 
   $template ||= $self->_agent_template;
 
 
   $template ||= $self->_agent_template;
 
@@ -1871,6 +2233,28 @@ sub print_latex {
   close $lh;
   $params{'logo_file'} = $lh->filename;
 
   close $lh;
   $params{'logo_file'} = $lh->filename;
 
+  if($conf->exists('invoice-barcode')){
+      my $png_file = $self->invoice_barcode($dir);
+      my $eps_file = $png_file;
+      $eps_file =~ s/\.png$/.eps/g;
+      $png_file =~ /(barcode.*png)/;
+      $png_file = $1;
+      $eps_file =~ /(barcode.*eps)/;
+      $eps_file = $1;
+
+      my $curr_dir = cwd();
+      chdir($dir); 
+      # after painfuly long experimentation, it was determined that sam2p won't
+      #        accept : and other chars in the path, no matter how hard I tried to
+      # escape them, hence the chdir (and chdir back, just to be safe)
+      system('sam2p', '-j:quiet', $png_file, 'EPS:', $eps_file ) == 0
+       or die "sam2p failed: $!\n";
+      unlink($png_file);
+      chdir($curr_dir);
+
+      $params{'barcode_file'} = $eps_file;
+  }
+
   my @filled_in = $self->print_generic( %params );
   
   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
   my @filled_in = $self->print_generic( %params );
   
   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
@@ -1882,11 +2266,40 @@ sub print_latex {
   close $fh;
 
   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
   close $fh;
 
   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
-  return ($1, $params{'logo_file'});
+  return ($1, $params{'logo_file'}, $params{'barcode_file'});
+
+}
+
+=item invoice_barcode DIR_OR_FALSE
 
 
+Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
+it is taken as the temp directory where the PNG file will be generated and the
+PNG file name is returned. Otherwise, the PNG image itself is returned.
+
+=cut
+
+sub invoice_barcode {
+    my ($self, $dir) = (shift,shift);
+    
+    my $gdbar = new GD::Barcode('Code39',$self->invnum);
+       die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
+    my $gd = $gdbar->plot(Height => 30);
+
+    if($dir) {
+       my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
+                          DIR      => $dir,
+                          SUFFIX   => '.png',
+                          UNLINK   => 0,
+                        ) or die "can't open temp file: $!\n";
+       print $bh $gd->png or die "cannot write barcode to file: $!\n";
+       my $png_file = $bh->filename;
+       close $bh;
+       return $png_file;
+    }
+    return $gd->png;
 }
 
 }
 
-=item print_generic OPTIONS_HASH
+=item print_generic OPTION => VALUE ...
 
 Internal method - returns a filled-in template for this invoice as a scalar.
 
 
 Internal method - returns a filled-in template for this invoice as a scalar.
 
@@ -1908,10 +2321,13 @@ cid -
 
 unsquelch_cdr - overrides any per customer cdr squelching when true
 
 
 unsquelch_cdr - overrides any per customer cdr squelching when true
 
+notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
 =cut
 
 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
-# (alignment?) problems to change them all to '%.2f' ?
+# (alignment in text invoice?) problems to change them all to '%.2f' ?
+# yes: fixed width (dot matrix) text printing will be borked
 sub print_generic {
 
   my( $self, %params ) = @_;
 sub print_generic {
 
   my( $self, %params ) = @_;
@@ -1933,11 +2349,14 @@ sub print_generic {
                      'template' => [ '{', '}' ],
                    );
 
                      'template' => [ '{', '}' ],
                    );
 
+  warn "$me print_generic creating template\n"
+    if $DEBUG > 1;
+
   #create the template
   my $template = $params{template} ? $params{template} : $self->_agent_template;
   my $templatefile = "invoice_$format";
   $templatefile .= "_$template"
   #create the template
   my $template = $params{template} ? $params{template} : $self->_agent_template;
   my $templatefile = "invoice_$format";
   $templatefile .= "_$template"
-    if length($template);
+    if length($template) && $conf->exists($templatefile."_$template");
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
 
   my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config data $templatefile";
 
@@ -1950,12 +2369,18 @@ sub print_generic {
     @invoice_template = _translate_old_latex_format(@invoice_template);
   } 
 
     @invoice_template = _translate_old_latex_format(@invoice_template);
   } 
 
+  warn "$me print_generic creating T:T object\n"
+    if $DEBUG > 1;
+
   my $text_template = new Text::Template(
     TYPE => 'ARRAY',
     SOURCE => \@invoice_template,
     DELIMITERS => $delimiters{$format},
   );
 
   my $text_template = new Text::Template(
     TYPE => 'ARRAY',
     SOURCE => \@invoice_template,
     DELIMITERS => $delimiters{$format},
   );
 
+  warn "$me print_generic compiling T:T object\n"
+    if $DEBUG > 1;
+
   $text_template->compile()
     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
 
   $text_template->compile()
     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
 
@@ -1968,6 +2393,7 @@ sub print_generic {
                  'smallfooter'   => sub { map "$_", @_ },
                  'returnaddress' => sub { map "$_", @_ },
                  'coupon'        => sub { map "$_", @_ },
                  'smallfooter'   => sub { map "$_", @_ },
                  'returnaddress' => sub { map "$_", @_ },
                  'coupon'        => sub { map "$_", @_ },
+                 'summary'       => sub { map "$_", @_ },
                },
     'html'  => {
                  'notes' =>
                },
     'html'  => {
                  'notes' =>
@@ -2001,6 +2427,7 @@ sub print_generic {
                      }  @_
                    },
                  'coupon'        => sub { "" },
                      }  @_
                    },
                  'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
                },
     'template' => {
                  'notes' =>
                },
     'template' => {
                  'notes' =>
@@ -2031,6 +2458,7 @@ sub print_generic {
                      }  @_
                    },
                  'coupon'        => sub { "" },
                      }  @_
                    },
                  'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
                },
   );
 
                },
   );
 
@@ -2043,15 +2471,19 @@ sub print_generic {
   my $nbsp = $nbsps{$format};
 
   my %escape_functions = ( 'latex'    => \&_latex_escape,
   my $nbsp = $nbsps{$format};
 
   my %escape_functions = ( 'latex'    => \&_latex_escape,
-                           'html'     => \&encode_entities,
+                           'html'     => \&_html_escape_nbsp,#\&encode_entities,
                            'template' => sub { shift },
                          );
   my $escape_function = $escape_functions{$format};
                            'template' => sub { shift },
                          );
   my $escape_function = $escape_functions{$format};
+  my $escape_function_nonbsp = ($format eq 'html')
+                                 ? \&_html_escape : $escape_function;
 
 
-  my %date_formats = ( 'latex'    => '%b %o, %Y',
-                       'html'     => '%b&nbsp;%o,&nbsp;%Y',
+  my %date_formats = ( 'latex'    => $date_format_long,
+                       'html'     => $date_format_long,
                        'template' => '%s',
                      );
                        'template' => '%s',
                      );
+  $date_formats{'html'} =~ s/ /&nbsp;/g;
+
   my $date_format = $date_formats{$format};
 
   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
   my $date_format = $date_formats{$format};
 
   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
@@ -2062,6 +2494,14 @@ sub print_generic {
                            );
   my $embolden_function = $embolden_functions{$format};
 
                            );
   my $embolden_function = $embolden_functions{$format};
 
+  my %newline_tokens = (  'latex'     => '\\\\',
+                          'html'      => '<br>',
+                          'template'  => "\n",
+                        );
+  my $newline_token = $newline_tokens{$format};
+
+  warn "$me generating template variables\n"
+    if $DEBUG > 1;
 
   # generate template variables
   my $returnaddress;
 
   # generate template variables
   my $returnaddress;
@@ -2115,37 +2555,89 @@ sub print_generic {
 
   }
 
 
   }
 
+  warn "$me generating invoice data\n"
+    if $DEBUG > 1;
+
+  my $agentnum = $self->cust_main->agentnum;
+
   my %invoice_data = (
   my %invoice_data = (
-    '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,
+
+    #invoice from info
+    'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
+    'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
+    'company_phonenum'=> scalar( $conf->config('company_phonenum', $agentnum) ),
+    'returnaddress'   => $returnaddress,
+    'agent'           => &$escape_function($cust_main->agent->agent),
+
+    #invoice info
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
     '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),
-    'address2'        => &$escape_function($cust_main->address2),
-    '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,
+    'today'           => time2str($date_format_long, $today),
     'terms'           => $self->terms,
     'template'        => $template, #params{'template'},
     'terms'           => $self->terms,
     '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,
+    'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
     'current_charges' => sprintf("%.2f", $self->charged),
     'current_charges' => sprintf("%.2f", $self->charged),
-    'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
+    'duedate'         => $self->due_date2str($rdate_format), #date_format?
+
+    #customer info
+    'custnum'         => $cust_main->display_custnum,
+    'agent_custid'    => &$escape_function($cust_main->agent_custid),
+    ( map { $_ => &$escape_function($cust_main->$_()) } qw(
+      payname company address1 address2 city state zip fax
+    )),
+
+    #global config
     'ship_enable'     => $conf->exists('invoice-ship_address'),
     'unitprices'      => $conf->exists('invoice-unitprice'),
     'ship_enable'     => $conf->exists('invoice-ship_address'),
     'unitprices'      => $conf->exists('invoice-unitprice'),
+    'smallernotes'    => $conf->exists('invoice-smallernotes'),
+    'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
+    'balance_due_below_line' => $conf->exists('balance_due_below_line'),
+   
+    #layout info -- would be fancy to calc some of this and bury the template
+    #               here in the code
+    'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
+    'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
+    'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
+    'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
+    'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
+    'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
+    'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
+    'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
+    'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
+    'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+
+    # better hang on to conf_dir for a while (for old templates)
+    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+
+    #these are only used when doing paged plaintext
+    'page'            => 1,
+    'total_pages'     => 1,
+
   );
   );
+  
+  my $min_sdate = 999999999999;
+  my $max_edate = 0;
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+    $min_sdate = $cust_bill_pkg->sdate
+      if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
+    $max_edate = $cust_bill_pkg->edate
+      if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
+  }
+
+  $invoice_data{'bill_period'} = '';
+  $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
+    . " to " . time2str('%e %h', $max_edate)
+    if ($max_edate != 0 && $min_sdate != 999999999999);
+
+  $invoice_data{finance_section} = '';
+  if ( $conf->config('finance_pkgclass') ) {
+    my $pkg_class =
+      qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
+    $invoice_data{finance_section} = $pkg_class->categoryname;
+  } 
+  $invoice_data{finance_amount} = '0.00';
+  $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
@@ -2188,18 +2680,34 @@ sub print_generic {
 
   $invoice_data{'logo_file'} = $params{'logo_file'}
     if $params{'logo_file'};
 
   $invoice_data{'logo_file'} = $params{'logo_file'}
     if $params{'logo_file'};
+  $invoice_data{'barcode_file'} = $params{'barcode_file'}
+    if $params{'barcode_file'};
+  $invoice_data{'barcode_img'} = $params{'barcode_img'}
+    if $params{'barcode_img'};
+  $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
+    if $params{'barcode_cid'};
 
   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( $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{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
-  my $agentnum = $self->cust_main->agentnum;
+  my $summarypage = '';
+  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+    $summarypage = 1;
+  }
+  $invoice_data{'summarypage'} = $summarypage;
 
 
-  #do variable substitution in notes, footer, smallfooter
-  foreach my $include (qw( notes footer smallfooter coupon )) {
+  warn "$me substituting variables in notes, footer, smallfooter\n"
+    if $DEBUG > 1;
+
+  my @include = (qw( notes footer smallfooter ));
+  push @include, 'coupon' unless $params{'no_coupon'};
+  foreach my $include (@include) {
 
     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
     my @inc_src;
 
     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
     my @inc_src;
@@ -2257,6 +2765,7 @@ sub print_generic {
                             'template' => '',
                           );
   my $other_money_char = $other_money_chars{$format};
                             'template' => '',
                           );
   my $other_money_char = $other_money_chars{$format};
+  $invoice_data{'dollar'} = $other_money_char;
 
   my @detail_items = ();
   my @total_items = ();
 
   my @detail_items = ();
   my @total_items = ();
@@ -2268,24 +2777,78 @@ sub print_generic {
   $invoice_data{'buf'} = \@buf;
   $invoice_data{'sections'} = \@sections;
 
   $invoice_data{'buf'} = \@buf;
   $invoice_data{'sections'} = \@sections;
 
+  warn "$me generating sections\n"
+    if $DEBUG > 1;
+
   my $previous_section = { 'description' => 'Previous Charges',
                            'subtotal'    => $other_money_char.
                                             sprintf('%.2f', $pr_total),
   my $previous_section = { 'description' => 'Previous Charges',
                            'subtotal'    => $other_money_char.
                                             sprintf('%.2f', $pr_total),
+                           'summarized'  => $summarypage ? 'Y' : '',
                          };
                          };
+  $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
+    join(' / ', map { $cust_main->balance_date_range(@$_) }
+                $self->_prior_month30s
+        )
+    if $conf->exists('invoice_include_aging');
 
   my $taxtotal = 0;
   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
 
   my $taxtotal = 0;
   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
-                      'subtotal'    => $taxtotal }; # adjusted below
+                      'subtotal'    => $taxtotal,   # adjusted below
+                      'summarized'  => $summarypage ? 'Y' : '',
+                    };
+  my $tax_weight = _pkg_category($tax_section->{description})
+                        ? _pkg_category($tax_section->{description})->weight
+                        : 0;
+  $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
+  $tax_section->{'sort_weight'} = $tax_weight;
+
 
   my $adjusttotal = 0;
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
 
   my $adjusttotal = 0;
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
-                         'subtotal'    => 0 }; # adjusted below
+                         'subtotal'    => 0,   # adjusted below
+                         'summarized'  => $summarypage ? 'Y' : '',
+                       };
+  my $adjust_weight = _pkg_category($adjust_section->{description})
+                        ? _pkg_category($adjust_section->{description})->weight
+                        : 0;
+  $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
+  $adjust_section->{'sort_weight'} = $adjust_weight;
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  $invoice_data{'multisection'} = $multisection;
   my $late_sections = [];
   my $late_sections = [];
+  my $extra_sections = [];
+  my $extra_lines = ();
   if ( $multisection ) {
   if ( $multisection ) {
-    push @sections, $self->_items_sections( $late_sections );
+    ($extra_sections, $extra_lines) =
+      $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
+      if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
+
+    push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
+
+    push @detail_items, @$extra_lines if $extra_lines;
+    push @sections,
+      $self->_items_sections( $late_sections,      # this could stand a refactor
+                              $summarypage,
+                              $escape_function_nonbsp,
+                              $extra_sections,
+                              $format,             #bah
+                            );
+    if ($conf->exists('svc_phone_sections')) {
+      my ($phone_sections, $phone_lines) =
+        $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
+      push @{$late_sections}, @$phone_sections;
+      push @detail_items, @$phone_lines;
+    }
+    if ($conf->exists('voip-cust_accountcode_cdr') && $cust_main->accountcode_cdr) {
+      my ($accountcode_section, $accountcode_lines) =
+        $self->_items_accountcode_cdr($escape_function_nonbsp,$format);
+      if ( scalar(@$accountcode_lines) ) {
+          push @{$late_sections}, $accountcode_section;
+          push @detail_items, @$accountcode_lines;
+      }
+    }
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
@@ -2295,6 +2858,9 @@ sub print_generic {
          )
   {
 
          )
   {
 
+    warn "$me adding previous balances\n"
+      if $DEBUG > 1;
+
     foreach my $line_item ( $self->_items_previous ) {
 
       my $detail = {
     foreach my $line_item ( $self->_items_previous ) {
 
       my $detail = {
@@ -2320,34 +2886,80 @@ sub print_generic {
     }
 
   }
     }
 
   }
-
+  
   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, ['',''];
   }
   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, ['',''];
   }
+  if ( $conf->exists('svc_phone-did-summary') ) {
+      warn "$me adding DID summary\n"
+        if $DEBUG > 1;
+
+      my ($didsummary,$minutes) = $self->_did_summary;
+      my $didsummary_desc = 'DID Activity Summary (since last invoice)';
+      push @detail_items, 
+       { 'description' => $didsummary_desc,
+           'ext_description' => [ $didsummary, $minutes ],
+       };
+  }
 
   foreach my $section (@sections, @$late_sections) {
 
 
   foreach my $section (@sections, @$late_sections) {
 
+    warn "$me adding section \n". Dumper($section)
+      if $DEBUG > 1;
+
+    # begin some normalization
+    $section->{'subtotal'} = $section->{'amount'}
+      if $multisection
+         && !exists($section->{subtotal})
+         && exists($section->{amount});
+
+    $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
+      if ( $invoice_data{finance_section} &&
+           $section->{'description'} eq $invoice_data{finance_section} );
+
     $section->{'subtotal'} = $other_money_char.
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
 
     $section->{'subtotal'} = $other_money_char.
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
 
+    # continue some normalization
+    $section->{'amount'}   = $section->{'subtotal'}
+      if $multisection;
+
+
     if ( $section->{'description'} ) {
       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
                    [ '', '' ],
                  );
     }
 
     if ( $section->{'description'} ) {
       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
                    [ '', '' ],
                  );
     }
 
+    warn "$me   setting options\n"
+      if $DEBUG > 1;
+
+    my $multilocation = scalar($cust_main->cust_location); #too expensive?
     my %options = ();
     $options{'section'} = $section if $multisection;
     $options{'format'} = $format;
     $options{'escape_function'} = $escape_function;
     $options{'format_function'} = sub { () } unless $unsquelched;
     $options{'unsquelched'} = $unsquelched;
     my %options = ();
     $options{'section'} = $section if $multisection;
     $options{'format'} = $format;
     $options{'escape_function'} = $escape_function;
     $options{'format_function'} = sub { () } unless $unsquelched;
     $options{'unsquelched'} = $unsquelched;
+    $options{'summary_page'} = $summarypage;
+    $options{'skip_usage'} =
+      scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
+    $options{'multilocation'} = $multilocation;
+    $options{'multisection'} = $multisection;
+
+    warn "$me   searching for line items\n"
+      if $DEBUG > 1;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
+
+      warn "$me     adding line item $line_item\n"
+        if $DEBUG > 1;
+
       my $detail = {
         ext_description => [],
       };
       my $detail = {
         ext_description => [],
       };
@@ -2384,10 +2996,18 @@ sub print_generic {
   
   }
   
   
   }
   
-  if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+  $invoice_data{current_less_finance} =
+    sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
+
+  if ( $multisection && !$conf->exists('disable_previous_balance')
+    || $conf->exists('previous_balance-summary_only') )
+  {
     unshift @sections, $previous_section if $pr_total;
   }
 
     unshift @sections, $previous_section if $pr_total;
   }
 
+  warn "$me adding taxes\n"
+    if $DEBUG > 1;
+
   foreach my $tax ( $self->_items_tax ) {
 
     $taxtotal += $tax->{'amount'};
   foreach my $tax ( $self->_items_tax ) {
 
     $taxtotal += $tax->{'amount'};
@@ -2451,32 +3071,34 @@ sub print_generic {
 
   {
     my $total = {};
 
   {
     my $total = {};
-    $total->{'total_item'} = &$embolden_function('Total');
+    my $item = 'Total';
+    $item = $conf->config('previous_balance-exclude_from_total')
+         || 'Total New Charges'
+      if $conf->exists('previous_balance-exclude_from_total');
+    my $amount = $self->charged +
+                   ( $conf->exists('disable_previous_balance') ||
+                     $conf->exists('previous_balance-exclude_from_total')
+                     ? 0
+                     : $pr_total
+                   );
+    $total->{'total_item'} = &$embolden_function($item);
     $total->{'total_amount'} =
     $total->{'total_amount'} =
-      &$embolden_function(
-        $other_money_char.
-        sprintf( '%.2f',
-                 $self->charged + ( $conf->exists('disable_previous_balance')
-                                    ? 0
-                                    : $pr_total
-                                  )
-               )
-      );
+      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
     if ( $multisection ) {
     if ( $multisection ) {
-      $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
-                                      sprintf('%.2f', $self->charged );
+      if ( $adjust_section->{'sort_weight'} ) {
+        $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
+          sprintf("%.2f", ($self->billing_balance || 0) );
+      } else {
+        $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
+                                        sprintf('%.2f', $self->charged );
+      } 
     }else{
       push @total_items, $total;
     }
     push @buf,['','-----------'];
     }else{
       push @total_items, $total;
     }
     push @buf,['','-----------'];
-    push @buf,['Total Charges',
+    push @buf,[$item,
                $money_char.
                $money_char.
-               sprintf( '%10.2f', $self->charged +
-                                    ( $conf->exists('disable_previous_balance')
-                                        ? 0
-                                        : $pr_total
-                                    )
-                      )
+               sprintf( '%10.2f', $amount )
               ];
     push @buf,['',''];
   }
               ];
     push @buf,['',''];
   }
@@ -2547,7 +3169,8 @@ sub print_generic {
     if ( $multisection ) {
       $adjust_section->{'subtotal'} = $other_money_char.
                                       sprintf('%.2f', $adjusttotal);
     if ( $multisection ) {
       $adjust_section->{'subtotal'} = $other_money_char.
                                       sprintf('%.2f', $adjusttotal);
-      push @sections, $adjust_section;
+      push @sections, $adjust_section
+        unless $adjust_section->{sort_weight};
     }
 
     { 
     }
 
     { 
@@ -2555,9 +3178,13 @@ sub print_generic {
       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
       $total->{'total_amount'} =
         &$embolden_function(
       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
       $total->{'total_amount'} =
         &$embolden_function(
-          $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
+          $other_money_char. sprintf('%.2f', $summarypage 
+                                               ? $self->charged +
+                                                 $self->billing_balance
+                                               : $self->owed + $pr_total
+                                    )
         );
         );
-      if ( $multisection ) {
+      if ( $multisection && !$adjust_section->{sort_weight} ) {
         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
                                          $total->{'total_amount'};
       }else{
         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
                                          $total->{'total_amount'};
       }else{
@@ -2567,13 +3194,88 @@ sub print_generic {
       push @buf,[$self->balance_due_msg, $money_char. 
         sprintf("%10.2f", $balance_due ) ];
     }
       push @buf,[$self->balance_due_msg, $money_char. 
         sprintf("%10.2f", $balance_due ) ];
     }
+
+    if ( $conf->exists('previous_balance-show_credit')
+        and $cust_main->balance < 0 ) {
+      my $credit_total = {
+        'total_item'    => &$embolden_function($self->credit_balance_msg),
+        'total_amount'  => &$embolden_function(
+          $other_money_char. sprintf('%.2f', -$cust_main->balance)
+        ),
+      };
+      if ( $multisection ) {
+        $adjust_section->{'posttotal'} .= $newline_token .
+          $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
+      }
+      else {
+        push @total_items, $credit_total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->credit_balance_msg, $money_char. 
+        sprintf("%10.2f", -$cust_main->balance ) ];
+    }
   }
 
   if ( $multisection ) {
   }
 
   if ( $multisection ) {
+    if ($conf->exists('svc_phone_sections')) {
+      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)
+        );
+      my $last_section = pop @sections;
+      $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                     $total->{'total_amount'};
+      push @sections, $last_section;
+    }
     push @sections, @$late_sections
       if $unsquelched;
   }
 
     push @sections, @$late_sections
       if $unsquelched;
   }
 
+  my @includelist = ();
+  push @includelist, 'summary' if $summarypage;
+  foreach my $include ( @includelist ) {
+
+    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
+    my @inc_src;
+
+    if ( length( $conf->config($inc_file, $agentnum) ) ) {
+
+      @inc_src = $conf->config($inc_file, $agentnum);
+
+    } else {
+
+      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+
+      my $convert_map = $convert_maps{$format}{$include};
+
+      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+                       s/--\@\]/$delimiters{$format}[1]/g;
+                       $_;
+                     } 
+                 &$convert_map( $conf->config($inc_file, $agentnum) );
+
+    }
+
+    my $inc_tt = new Text::Template (
+      TYPE       => 'ARRAY',
+      SOURCE     => [ map "$_\n", @inc_src ],
+      DELIMITERS => $delimiters{$format},
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+    unless ( $inc_tt->compile() ) {
+      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+      die $error;
+    }
+
+    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+    $invoice_data{$include} =~ s/\n+$//
+      if ($format eq 'latex');
+  }
+
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
@@ -2624,58 +3326,92 @@ sub print_generic {
   }
 }
 
   }
 }
 
-=item print_ps [ TIME [ , TEMPLATE ] ]
+# helper routine for generating date ranges
+sub _prior_month30s {
+  my $self = shift;
+  my @ranges = (
+   [ 1,       2592000 ], # 0-30 days ago
+   [ 2592000, 5184000 ], # 30-60 days ago
+   [ 5184000, 7776000 ], # 60-90 days ago
+   [ 7776000, 0       ], # 90+   days ago
+  );
+
+  map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
+          $_->[1] ? $self->_date - $_->[1] - 1 : '',
+      ] }
+  @ranges;
+}
+
+=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
 
 
 Returns an postscript invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_ps {
   my $self = shift;
 
 =cut
 
 sub print_ps {
   my $self = shift;
 
-  my ($file, $lfile) = $self->print_latex(@_);
+  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
   my $ps = generate_ps($file);
   my $ps = generate_ps($file);
-  unlink($lfile);
+  unlink($logofile);
+  unlink($barcodefile) if $barcodefile;
 
   $ps;
 }
 
 
   $ps;
 }
 
-=item print_pdf [ TIME [ , TEMPLATE ] ]
+=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an PDF invoice, as a scalar.
 
 
 Returns an PDF invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_pdf {
   my $self = shift;
 
 =cut
 
 sub print_pdf {
   my $self = shift;
 
-  my ($file, $lfile) = $self->print_latex(@_);
+  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
   my $pdf = generate_pdf($file);
   my $pdf = generate_pdf($file);
-  unlink($lfile);
+  unlink($logofile);
+  unlink($barcodefile) if $barcodefile;
 
   $pdf;
 }
 
 
   $pdf;
 }
 
-=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
+=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
 
 Returns an HTML invoice, as a scalar.
 
 
 Returns an HTML invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
-CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
 when emailing the invoice as part of a multipart/related MIME email.
 
 =cut
 when emailing the invoice as part of a multipart/related MIME email.
 
 =cut
@@ -2683,7 +3419,7 @@ when emailing the invoice as part of a multipart/related MIME email.
 sub print_html {
   my $self = shift;
   my %params;
 sub print_html {
   my $self = shift;
   my %params;
-  if ( ref $_[0]  ) {
+  if ( ref($_[0]) ) {
     %params = %{ shift() }; 
   }else{
     $params{'time'} = shift;
     %params = %{ shift() }; 
   }else{
     $params{'time'} = shift;
@@ -2692,7 +3428,7 @@ sub print_html {
   }
 
   $params{'format'} = 'html';
   }
 
   $params{'format'} = 'html';
-
+  
   $self->print_generic( %params );
 }
 
   $self->print_generic( %params );
 }
 
@@ -2712,6 +3448,18 @@ sub _latex_escape {
   $value;
 }
 
   $value;
 }
 
+sub _html_escape {
+  my $value = shift;
+  encode_entities($value);
+  $value;
+}
+
+sub _html_escape_nbsp {
+  my $value = _html_escape(shift);
+  $value =~ s/ +/&nbsp;/g;
+  $value;
+}
+
 #utility methods for print_*
 
 sub _translate_old_latex_format {
 #utility methods for print_*
 
 sub _translate_old_latex_format {
@@ -2741,10 +3489,10 @@ sub _translate_old_latex_format {
         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
         push @template, "    \$OUT .= '$line_item_line';";
       }
         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
         push @template, "    \$OUT .= '$line_item_line';";
       }
-  
+
       push @template, '}',
                       '--@]';
       push @template, '}',
                       '--@]';
-
+      #' doh, gvim
     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
 
       push @template, '[@--',
     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
 
       push @template, '[@--',
@@ -2778,11 +3526,12 @@ sub _translate_old_latex_format {
 sub terms {
   my $self = shift;
 
 sub terms {
   my $self = shift;
 
-  #check for an invoice- specific override (eventually)
+  #check for an invoice-specific override
+  return $self->invoice_terms if $self->invoice_terms;
   
   #check for a customer- specific override
   
   #check for a customer- specific override
-  return $self->cust_main->invoice_terms
-    if $self->cust_main->invoice_terms;
+  my $cust_main = $self->cust_main;
+  return $cust_main->invoice_terms if $cust_main->invoice_terms;
 
   #use configured default
   $conf->config('invoice_default_terms') || '';
 
   #use configured default
   $conf->config('invoice_default_terms') || '';
@@ -2807,7 +3556,7 @@ sub balance_due_msg {
   my $msg = 'Balance Due';
   return $msg unless $self->terms;
   if ( $self->due_date ) {
   my $msg = 'Balance Due';
   return $msg unless $self->terms;
   if ( $self->due_date ) {
-    $msg .= ' - Please pay by '. $self->due_date2str('%x');
+    $msg .= ' - Please pay by '. $self->due_date2str($date_format);
   } elsif ( $self->terms ) {
     $msg .= ' - '. $self->terms;
   }
   } elsif ( $self->terms ) {
     $msg .= ' - '. $self->terms;
   }
@@ -2819,11 +3568,13 @@ sub balance_due_date {
   my $duedate = '';
   if (    $conf->exists('invoice_default_terms') 
        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
   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 = time2str($rdate_format, $self->_date + ($1*86400) );
   }
   $duedate;
 }
 
   }
   $duedate;
 }
 
+sub credit_balance_msg { 'Credit Balance Remaining' }
+
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
@@ -2844,80 +3595,854 @@ Returns a string with the date, for example: "3/20/2008"
 
 sub _date_pretty {
   my $self = shift;
 
 sub _date_pretty {
   my $self = shift;
-  time2str('%x', $self->_date);
+  time2str($date_format, $self->_date);
 }
 
 }
 
+use vars qw(%pkg_category_cache);
 sub _items_sections {
   my $self = shift;
   my $late = shift;
 sub _items_sections {
   my $self = shift;
   my $late = shift;
+  my $summarypage = shift;
+  my $escape = shift;
+  my $extra_sections = shift;
+  my $format = shift;
 
 
-  my %s = ();
-  my %l = ();
+  my %subtotal = ();
+  my %late_subtotal = ();
+  my %not_tax = ();
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
   {
 
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
   {
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
       my $usage = $cust_bill_pkg->usage;
 
       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
       my $usage = $cust_bill_pkg->usage;
 
       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
-        my $desc = $display->section;
-        my $type = $display->type;
+        next if ( $display->summary && $summarypage );
+
+        my $section = $display->section;
+        my $type    = $display->type;
+
+        $not_tax{$section} = 1
+          unless $cust_bill_pkg->pkgnum == 0;
 
 
-        if ( $display->post_total ) {
+        if ( $display->post_total && !$summarypage ) {
           if (! $type || $type eq 'S') {
           if (! $type || $type eq 'S') {
-            $l{$desc} += $cust_bill_pkg->setup
-              if ( $cust_bill_pkg->setup != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0;
           }
 
           if (! $type) {
           }
 
           if (! $type) {
-            $l{$desc} += $cust_bill_pkg->recur
-              if ( $cust_bill_pkg->recur != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0;
           }
 
           if ($type && $type eq 'R') {
           }
 
           if ($type && $type eq 'R') {
-            $l{$desc} += $cust_bill_pkg->recur - $usage
-              if ( $cust_bill_pkg->recur != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0;
           }
           
           if ($type && $type eq 'U') {
           }
           
           if ($type && $type eq 'U') {
-            $l{$desc} += $usage;
+            $late_subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
           }
 
         } else {
           }
 
         } else {
+
+          next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
           if (! $type || $type eq 'S') {
           if (! $type || $type eq 'S') {
-            $s{$desc} += $cust_bill_pkg->setup
-              if ( $cust_bill_pkg->setup != 0 );
+            $subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0;
           }
 
           if (! $type) {
           }
 
           if (! $type) {
-            $s{$desc} += $cust_bill_pkg->recur
-              if ( $cust_bill_pkg->recur != 0 );
+            $subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0;
           }
 
           if ($type && $type eq 'R') {
           }
 
           if ($type && $type eq 'R') {
-            $s{$desc} += $cust_bill_pkg->recur - $usage
-              if ( $cust_bill_pkg->recur != 0 );
+            $subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0;
           }
           
           if ($type && $type eq 'U') {
           }
           
           if ($type && $type eq 'U') {
-            $s{$desc} += $usage;
+            $subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
           }
 
         }
 
       }
 
           }
 
         }
 
       }
 
+  }
+
+  %pkg_category_cache = ();
+
+  push @$late, map { { 'description' => &{$escape}($_),
+                       'subtotal'    => $late_subtotal{$_},
+                       'post_total'  => 1,
+                       'sort_weight' => ( _pkg_category($_)
+                                            ? _pkg_category($_)->weight
+                                            : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
+                   } }
+                 sort _sectionsort keys %late_subtotal;
+
+  my @sections;
+  if ( $summarypage ) {
+    @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
+                map { $_->categoryname } qsearch('pkg_category', {});
+    push @sections, '' if exists($subtotal{''});
+  } else {
+    @sections = keys %subtotal;
+  }
+
+  my @early = map { { 'description' => &{$escape}($_),
+                      'subtotal'    => $subtotal{$_},
+                      'summarized'  => $not_tax{$_} ? '' : 'Y',
+                      'tax_section' => $not_tax{$_} ? '' : 'Y',
+                      'sort_weight' => ( _pkg_category($_)
+                                           ? _pkg_category($_)->weight
+                                           : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
+                                           ? $self->_condense_section($format)
+                                           : ()
+                       ),
+                    }
+                  } @sections;
+  push @early, @$extra_sections if $extra_sections;
+
+  sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
+
+}
+
+#helper subs for above
+
+sub _sectionsort {
+  _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+}
+
+sub _pkg_category {
+  my $categoryname = shift;
+  $pkg_category_cache{$categoryname} ||=
+    qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
+}
+
+my %condensed_format = (
+  'label' => [ qw( Description Qty Amount ) ],
+  'fields' => [
+                sub { shift->{description} },
+                sub { shift->{quantity} },
+                sub { my($href, %opt) = @_;
+                      ($opt{dollar} || ''). $href->{amount};
+                    },
+              ],
+  'align'  => [ qw( l r r ) ],
+  'span'   => [ qw( 5 1 1 ) ],            # unitprices?
+  'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
+);
+
+sub _condense_section {
+  my ( $self, $format ) = ( shift, shift );
+  ( 'condensed' => 1,
+    map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
+      qw( description_generator
+          header_generator
+          total_generator
+          total_line_generator
+        )
+  );
+}
+
+sub _condensed_generator_defaults {
+  my ( $self, $format ) = ( shift, shift );
+  return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
+}
+
+my %html_align = (
+  'c' => 'center',
+  'l' => 'left',
+  'r' => 'right',
+);
+
+sub _condensed_header_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+
+  if ($format eq 'latex') {
+    $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
+    $suffix = "\\\\\n\\hline";
+    $separator = "&\n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+          };
+  } elsif ( $format eq 'html' ) {
+    $prefix = '<th></th>';
+    $suffix = '';
+    $separator = '';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<th align="$html_align{$a}">$d</th>!;
+      };
+  }
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
     }
 
     }
 
+    $prefix. join($separator, @result). $suffix;
+  };
+
+}
+
+sub _condensed_description_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+
+  my $money_char = '$';
+  if ($format eq 'latex') {
+    $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
+    $suffix = '\\\\';
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
+          };
+    $money_char = '\\dollar';
+  }elsif ( $format eq 'html' ) {
+    $prefix = '"><td align="center"></td>';
+    $suffix = '';
+    $separator = '';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}">$d</td>!;
+      };
+    #$money_char = $conf->config('money_char') || '$';
+    $money_char = '';  # this is madness
   }
 
   }
 
-  push @$late, map { { 'description' => $_,
-                       'subtotal'    => $l{$_},
-                       'post_total'  => 1,
-                   } } sort keys %l;
+  sub {
+    #my @args = @_;
+    my $href = shift;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      my $dollar = '';
+      $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
+
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+sub _condensed_total_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+  my $style = '';
+
+  if ($format eq 'latex') {
+    $prefix = "& ";
+    $suffix = "\\\\\n";
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+          };
+  }elsif ( $format eq 'html' ) {
+    $prefix = '';
+    $suffix = '';
+    $separator = '';
+    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+      };
+  }
+
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    #  my $r = &{$f->{fields}->[$i]}(@args);
+    #  $r .= ' Total' unless $i;
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
+
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+=item total_line_generator FORMAT
+
+Returns a coderef used for generation of invoice total line items for this
+usage_class.  FORMAT is either html or latex
+
+=cut
+
+# should not be used: will have issues with hash element names (description vs
+# total_item and amount vs total_amount -- another array of functions?
+
+sub _condensed_total_line_generator {
+  my ( $self, $format ) = ( shift, shift );
+
+  my ( $f, $prefix, $suffix, $separator, $column ) =
+    _condensed_generator_defaults($format);
+  my $style = '';
+
+  if ($format eq 'latex') {
+    $prefix = "& ";
+    $suffix = "\\\\\n";
+    $separator = " & \n";
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
+          };
+  }elsif ( $format eq 'html' ) {
+    $prefix = '';
+    $suffix = '';
+    $separator = '';
+    $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
+    $column =
+      sub { my ($d,$a,$s,$w) = @_;
+            return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
+      };
+  }
+
+
+  sub {
+    my @args = @_;
+    my @result = ();
+
+    foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}(@args),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
+    }
 
 
-  map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
+    $prefix. join( $separator, @result ). $suffix;
+  };
+
+}
+
+#sub _items_extra_usage_sections {
+#  my $self = shift;
+#  my $escape = shift;
+#
+#  my %sections = ();
+#
+#  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
+#  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+#  {
+#    next unless $cust_bill_pkg->pkgnum > 0;
+#
+#    foreach my $section ( keys %usage_class ) {
+#
+#      my $usage = $cust_bill_pkg->usage($section);
+#
+#      next unless $usage && $usage > 0;
+#
+#      $sections{$section} ||= 0;
+#      $sections{$section} += $usage;
+#
+#    }
+#
+#  }
+#
+#  map { { 'description' => &{$escape}($_),
+#          'subtotal'    => $sections{$_},
+#          'summarized'  => '',
+#          'tax_section' => '',
+#        }
+#      }
+#    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
+
+sub _items_extra_usage_sections {
+  my $self = shift;
+  my $escape = shift;
+  my $format = shift;
+
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
+
+  my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+
+    foreach my $classnum ( keys %usage_class ) {
+      my $section = $usage_class{$classnum}->classname;
+      $classnums{$section} = $classnum;
+
+      foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+        my $amount = $detail->amount;
+        next unless $amount && $amount > 0;
+        $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+        $sections{$section}{amount} += $amount;  #subtotal
+        $sections{$section}{calls}++;
+        $sections{$section}{duration} += $detail->duration;
+
+        my $desc = $detail->regionname; 
+        my $description = $desc;
+        $description = substr($desc, 0, 50). '...'
+          if $format eq 'latex' && length($desc) > 50;
+
+        $lines{$section}{$desc} ||= {
+          description     => &{$escape}($description),
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_bill_pkg->pkgnum,
+          ref             => '',
+          amount          => 0,
+          calls           => 0,
+          duration        => 0,
+          #unit_amount     => $cust_bill_pkg->unitrecur,
+          quantity        => $cust_bill_pkg->quantity,
+          product_code    => 'N/A',
+          ext_description => [],
+        };
+
+        $lines{$section}{$desc}{amount} += $amount;
+        $lines{$section}{$desc}{calls}++;
+        $lines{$section}{$desc}{duration} += $detail->duration;
+
+      }
+    }
+  }
+
+  my %sectionmap = ();
+  foreach (keys %sections) {
+    my $usage_class = $usage_class{$classnums{$_}};
+    $sectionmap{$_} = { 'description' => &{$escape}($_),
+                        'amount'    => $sections{$_}{amount},    #subtotal
+                        'calls'       => $sections{$_}{calls},
+                        'duration'    => $sections{$_}{duration},
+                        'summarized'  => '',
+                        'tax_section' => '',
+                        'sort_weight' => $usage_class->weight,
+                        ( $usage_class->format
+                          ? ( map { $_ => $usage_class->$_($format) }
+                              qw( description_generator header_generator total_generator total_line_generator )
+                            )
+                          : ()
+                        ), 
+                      };
+  }
+
+  my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+                 values %sectionmap;
+
+  my @lines = ();
+  foreach my $section ( keys %lines ) {
+    foreach my $line ( keys %{$lines{$section}} ) {
+      my $l = $lines{$section}{$line};
+      $l->{section}     = $sectionmap{$section};
+      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
+      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+      push @lines, $l;
+    }
+  }
+
+  return(\@sections, \@lines);
+
+}
+
+sub _did_summary {
+    my $self = shift;
+    my $end = $self->_date;
+
+    # start at date of previous invoice + 1 second or 0 if no previous invoice
+    my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
+    $start = 0 if !$start;
+    $start++;
+
+    my $cust_main = $self->cust_main;
+    my @pkgs = $cust_main->all_pkgs;
+    my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+       = (0,0,0,0,0);
+    my @seen = ();
+    foreach my $pkg ( @pkgs ) {
+       my @h_cust_svc = $pkg->h_cust_svc($end);
+       foreach my $h_cust_svc ( @h_cust_svc ) {
+           next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+           next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+
+           my $inserted = $h_cust_svc->date_inserted;
+           my $deleted = $h_cust_svc->date_deleted;
+           my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
+           my $phone_deleted;
+           $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
+           
+# DID either activated or ported in; cannot be both for same DID simultaneously
+           if ($inserted >= $start && $inserted <= $end && $phone_inserted
+               && (!$phone_inserted->lnp_status 
+                   || $phone_inserted->lnp_status eq ''
+                   || $phone_inserted->lnp_status eq 'native')) {
+               $num_activated++;
+           }
+           else { # this one not so clean, should probably move to (h_)svc_phone
+                my $phone_portedin = qsearchs( 'h_svc_phone',
+                     { 'svcnum' => $h_cust_svc->svcnum, 
+                       'lnp_status' => 'portedin' },  
+                     FS::h_svc_phone->sql_h_searchs($end),  
+                   );
+                $num_portedin++ if $phone_portedin;
+           }
+
+# DID either deactivated or ported out;        cannot be both for same DID simultaneously
+           if($deleted >= $start && $deleted <= $end && $phone_deleted
+               && (!$phone_deleted->lnp_status 
+                   || $phone_deleted->lnp_status ne 'portingout')) {
+               $num_deactivated++;
+           } 
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
+               && $phone_deleted->lnp_status 
+               && $phone_deleted->lnp_status eq 'portingout') {
+               $num_portedout++;
+           }
+
+           # increment usage minutes
+           my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
+           foreach my $cdr ( @cdrs ) {
+               $minutes += $cdr->billsec/60;
+           }
+
+           # don't look at this service again
+           push @seen, $h_cust_svc->svcnum;
+       }
+    }
+
+    $minutes = sprintf("%d", $minutes);
+    ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
+       . "$num_deactivated  Ported-Out: $num_portedout ",
+           "Total Minutes: $minutes");
+}
+
+sub _items_accountcode_cdr {
+    my $self = shift;
+    my $escape = shift;
+    my $format = shift;
+
+    my $section = { 'amount'        => 0,
+                    'calls'         => 0,
+                    'duration'      => 0,
+                    'sort_weight'   => '',
+                    'phonenum'      => '',
+                    'description'   => 'Usage by Account Code',
+                    'post_total'    => '',
+                    'summarized'    => '',
+                    'header'        => '',
+                  };
+    my @lines;
+    my %accountcodes = ();
+
+    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+        next unless $cust_bill_pkg->pkgnum > 0;
+
+        my @header = $cust_bill_pkg->details_header;
+        next unless scalar(@header);
+        $section->{'header'} = join(',',@header);
+
+        foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+            $section->{'header'} = $detail->formatted('format' => $format)
+                if($detail->detail eq $section->{'header'}); 
+      
+            my $accountcode = $detail->accountcode;
+            next unless $accountcode;
+
+            my $amount = $detail->amount;
+            next unless $amount && $amount > 0;
+
+            $accountcodes{$accountcode} ||= {
+                    description => $accountcode,
+                    pkgnum      => '',
+                    ref         => '',
+                    amount      => 0,
+                    calls       => 0,
+                    duration    => 0,
+                    quantity    => '',
+                    product_code => 'N/A',
+                    section     => $section,
+                    ext_description => [],
+            };
+
+            $section->{'amount'} += $amount;
+            $accountcodes{$accountcode}{'amount'} += $amount;
+            $accountcodes{$accountcode}{calls}++;
+            $accountcodes{$accountcode}{duration} += $detail->duration;
+            push @{$accountcodes{$accountcode}{ext_description}},
+                $detail->formatted('format' => $format);
+        }
+    }
+
+    foreach my $l ( values %accountcodes ) {
+        $l->{amount} = sprintf( "%.2f", $l->{amount} );
+        unshift @{$l->{ext_description}}, $section->{'header'};
+        push @lines, $l;
+    }
+
+    my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
+
+    return ($section,\@sorted_lines);
+}
+
+sub _items_svc_phone_sections {
+  my $self = shift;
+  my $escape = shift;
+  my $format = shift;
+
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
+
+  my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+  $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
+
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+
+    my @header = $cust_bill_pkg->details_header;
+    next unless scalar(@header);
+
+    foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+      my $phonenum = $detail->phonenum;
+      next unless $phonenum;
+
+      my $amount = $detail->amount;
+      next unless $amount && $amount > 0;
+
+      $sections{$phonenum} ||= { 'amount'      => 0,
+                                 'calls'       => 0,
+                                 'duration'    => 0,
+                                 'sort_weight' => -1,
+                                 'phonenum'    => $phonenum,
+                                };
+      $sections{$phonenum}{amount} += $amount;  #subtotal
+      $sections{$phonenum}{calls}++;
+      $sections{$phonenum}{duration} += $detail->duration;
+
+      my $desc = $detail->regionname; 
+      my $description = $desc;
+      $description = substr($desc, 0, 50). '...'
+        if $format eq 'latex' && length($desc) > 50;
+
+      $lines{$phonenum}{$desc} ||= {
+        description     => &{$escape}($description),
+        #pkgpart         => $part_pkg->pkgpart,
+        pkgnum          => '',
+        ref             => '',
+        amount          => 0,
+        calls           => 0,
+        duration        => 0,
+        #unit_amount     => '',
+        quantity        => '',
+        product_code    => 'N/A',
+        ext_description => [],
+      };
+
+      $lines{$phonenum}{$desc}{amount} += $amount;
+      $lines{$phonenum}{$desc}{calls}++;
+      $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+      my $line = $usage_class{$detail->classnum}->classname;
+      $sections{"$phonenum $line"} ||=
+        { 'amount' => 0,
+          'calls' => 0,
+          'duration' => 0,
+          'sort_weight' => $usage_class{$detail->classnum}->weight,
+          'phonenum' => $phonenum,
+          'header'  => [ @header ],
+        };
+      $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
+      $sections{"$phonenum $line"}{calls}++;
+      $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+      $lines{"$phonenum $line"}{$desc} ||= {
+        description     => &{$escape}($description),
+        #pkgpart         => $part_pkg->pkgpart,
+        pkgnum          => '',
+        ref             => '',
+        amount          => 0,
+        calls           => 0,
+        duration        => 0,
+        #unit_amount     => '',
+        quantity        => '',
+        product_code    => 'N/A',
+        ext_description => [],
+      };
+
+      $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+      $lines{"$phonenum $line"}{$desc}{calls}++;
+      $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+      push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+           $detail->formatted('format' => $format);
+
+    }
+  }
+
+  my %sectionmap = ();
+  my $simple = new FS::usage_class { format => 'simple' }; #bleh
+  foreach ( keys %sections ) {
+    my @header = @{ $sections{$_}{header} || [] };
+    my $usage_simple =
+      new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
+    my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+    my $usage_class = $summary ? $simple : $usage_simple;
+    my $ending = $summary ? ' usage charges' : '';
+    my %gen_opt = ();
+    unless ($summary) {
+      $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+    }
+    $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+                        'amount'    => $sections{$_}{amount},    #subtotal
+                        'calls'       => $sections{$_}{calls},
+                        'duration'    => $sections{$_}{duration},
+                        'summarized'  => '',
+                        'tax_section' => '',
+                        'phonenum'    => $sections{$_}{phonenum},
+                        'sort_weight' => $sections{$_}{sort_weight},
+                        'post_total'  => $summary, #inspire pagebreak
+                        (
+                          ( map { $_ => $usage_class->$_($format, %gen_opt) }
+                            qw( description_generator
+                                header_generator
+                                total_generator
+                                total_line_generator
+                              )
+                          )
+                        ), 
+                      };
+  }
+
+  my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+                        $a->{sort_weight} <=> $b->{sort_weight}
+                      }
+                 values %sectionmap;
+
+  my @lines = ();
+  foreach my $section ( keys %lines ) {
+    foreach my $line ( keys %{$lines{$section}} ) {
+      my $l = $lines{$section}{$line};
+      $l->{section}     = $sectionmap{$section};
+      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
+      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+      push @lines, $l;
+    }
+  }
+  
+  if($conf->exists('phone_usage_class_summary')) { 
+      # this only works with Latex
+      my @newlines;
+      my @newsections;
+
+      # after this, we'll have only two sections per DID:
+      # Calls Summary and Calls Detail
+      foreach my $section ( @sections ) {
+       if($section->{'post_total'}) {
+           $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+           $section->{'total_line_generator'} = sub { '' };
+           $section->{'total_generator'} = sub { '' };
+           $section->{'header_generator'} = sub { '' };
+           $section->{'description_generator'} = '';
+           push @newsections, $section;
+           my %calls_detail = %$section;
+           $calls_detail{'post_total'} = '';
+           $calls_detail{'sort_weight'} = '';
+           $calls_detail{'description_generator'} = sub { '' };
+           $calls_detail{'header_generator'} = sub {
+               return ' & Date/Time & Called Number & Duration & Price'
+                   if $format eq 'latex';
+               '';
+           };
+           $calls_detail{'description'} = 'Calls Detail: '
+                                                   . $section->{'phonenum'};
+           push @newsections, \%calls_detail;  
+       }
+      }
+
+      # after this, each usage class is collapsed/summarized into a single
+      # line under the Calls Summary section
+      foreach my $newsection ( @newsections ) {
+       if($newsection->{'post_total'}) { # this means Calls Summary
+           foreach my $section ( @sections ) {
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
+                               && !$section->{'post_total'});
+               my $newdesc = $section->{'description'};
+               my $tn = $section->{'phonenum'};
+               $newdesc =~ s/$tn//g;
+               my $line = {  ext_description => [],
+                             pkgnum => '',
+                             ref => '',
+                             quantity => '',
+                             calls => $section->{'calls'},
+                             section => $newsection,
+                             duration => $section->{'duration'},
+                             description => $newdesc,
+                             amount => sprintf("%.2f",$section->{'amount'}),
+                             product_code => 'N/A',
+                           };
+               push @newlines, $line;
+           }
+       }
+      }
+
+      # after this, Calls Details is populated with all CDRs
+      foreach my $newsection ( @newsections ) {
+       if(!$newsection->{'post_total'}) { # this means Calls Details
+           foreach my $line ( @lines ) {
+               next unless (scalar(@{$line->{'ext_description'}}) &&
+                       $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
+                           );
+               my @extdesc = @{$line->{'ext_description'}};
+               my @newextdesc;
+               foreach my $extdesc ( @extdesc ) {
+                   $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
+                   push @newextdesc, $extdesc;
+               }
+               $line->{'ext_description'} = \@newextdesc;
+               $line->{'section'} = $newsection;
+               push @newlines, $line;
+           }
+       }
+      }
+
+      return(\@newsections, \@newlines);
+  }
+
+  return(\@sections, \@lines);
 
 }
 
 
 }
 
@@ -2944,9 +4469,11 @@ sub _items_previous {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my @b = ();
   foreach ( @pr_cust_bill ) {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my @b = ();
   foreach ( @pr_cust_bill ) {
+    my $date = $conf->exists('invoice_show_prior_due_date')
+               ? 'due '. $_->due_date2str($date_format)
+               : time2str($date_format, $_->_date);
     push @b, {
     push @b, {
-      'description' => 'Previous Balance, Invoice #'. $_->invnum. 
-                       ' ('. time2str('%x',$_->_date). ')',
+      'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
       'amount'      => sprintf("%.2f", $_->owed),
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
       'amount'      => sprintf("%.2f", $_->owed),
@@ -2970,17 +4497,55 @@ sub _items_previous {
 
 sub _items_pkg {
   my $self = shift;
 
 sub _items_pkg {
   my $self = shift;
+  my %options = @_;
+
+  warn "$me _items_pkg searching for all package line items\n"
+    if $DEBUG > 1;
+
   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+  warn "$me _items_pkg filtering line items\n"
+    if $DEBUG > 1;
+  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+
+  if ($options{section} && $options{section}->{condensed}) {
+
+    warn "$me _items_pkg condensing section\n"
+      if $DEBUG > 1;
+
+    my %itemshash = ();
+    local $Storable::canonical = 1;
+    foreach ( @items ) {
+      my $item = { %$_ };
+      delete $item->{ref};
+      delete $item->{ext_description};
+      my $key = freeze($item);
+      $itemshash{$key} ||= 0;
+      $itemshash{$key} ++; # += $item->{quantity};
+    }
+    @items = sort { $a->{description} cmp $b->{description} }
+             map { my $i = thaw($_);
+                   $i->{quantity} = $itemshash{$_};
+                   $i->{amount} =
+                     sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
+                   $i;
+                 }
+             keys %itemshash;
+  }
+
+  warn "$me _items_pkg returning ". scalar(@items). " items\n"
+    if $DEBUG > 1;
+
+  @items;
 }
 
 sub _taxsort {
 }
 
 sub _taxsort {
-  return 0 unless $a cmp $b;
-  return -1 if $b eq 'Tax';
-  return 1 if $a eq 'Tax';
-  return -1 if $b eq 'Other surcharges';
-  return 1 if $a eq 'Other surcharges';
-  $a cmp $b;
+  return 0 unless $a->itemdesc cmp $b->itemdesc;
+  return -1 if $b->itemdesc eq 'Tax';
+  return 1 if $a->itemdesc eq 'Tax';
+  return -1 if $b->itemdesc eq 'Other surcharges';
+  return 1 if $a->itemdesc eq 'Other surcharges';
+  $a->itemdesc cmp $b->itemdesc;
 }
 
 sub _items_tax {
 }
 
 sub _items_tax {
@@ -2991,7 +4556,7 @@ sub _items_tax {
 
 sub _items_cust_bill_pkg {
   my $self = shift;
 
 sub _items_cust_bill_pkg {
   my $self = shift;
-  my $cust_bill_pkg = shift;
+  my $cust_bill_pkgs = shift;
   my %opt = @_;
 
   my $format = $opt{format} || '';
   my %opt = @_;
 
   my $format = $opt{format} || '';
@@ -2999,29 +4564,33 @@ sub _items_cust_bill_pkg {
   my $format_function = $opt{format_function} || '';
   my $unsquelched = $opt{unsquelched} || '';
   my $section = $opt{section}->{description} if $opt{section};
   my $format_function = $opt{format_function} || '';
   my $unsquelched = $opt{unsquelched} || '';
   my $section = $opt{section}->{description} if $opt{section};
+  my $summary_page = $opt{summary_page} || '';
+  my $multilocation = $opt{multilocation} || '';
+  my $multisection = $opt{multisection} || '';
+  my $discount_show_always = 0;
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
-  foreach my $cust_bill_pkg ( @$cust_bill_pkg )
+  foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
   {
 
   {
 
-    foreach ( $s, $r, $u ) {
-      if ( $_ && !$cust_bill_pkg->hidden ) {
-        $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
-        $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-        push @b, { %$_ };
-        $_ = undef;
-      }
-    }
+    warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
+         $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
+      if $DEBUG > 1;
 
     foreach my $display ( grep { defined($section)
                                  ? $_->section eq $section
                                  : 1
                                }
 
     foreach my $display ( grep { defined($section)
                                  ? $_->section eq $section
                                  : 1
                                }
+                          #grep { !$_->summary || !$summary_page } # bunk!
+                          grep { !$_->summary || $multisection }
                           $cust_bill_pkg->cust_bill_pkg_display
                         )
     {
 
                           $cust_bill_pkg->cust_bill_pkg_display
                         )
     {
 
+      warn "$me _items_cust_bill_pkg considering display item $display\n"
+        if $DEBUG > 1;
+
       my $type = $display->type;
 
       my $desc = $cust_bill_pkg->desc;
       my $type = $display->type;
 
       my $desc = $cust_bill_pkg->desc;
@@ -3035,18 +4604,37 @@ sub _items_cust_bill_pkg {
 
       if ( $cust_bill_pkg->pkgnum > 0 ) {
 
 
       if ( $cust_bill_pkg->pkgnum > 0 ) {
 
+        warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
+          if $DEBUG > 1;
         my $cust_pkg = $cust_bill_pkg->cust_pkg;
 
         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
 
         my $cust_pkg = $cust_bill_pkg->cust_pkg;
 
         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
 
+          warn "$me _items_cust_bill_pkg adding setup\n"
+            if $DEBUG > 1;
+
           my $description = $desc;
           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
 
           my @d = ();
           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;
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->hidden )
+          {
+
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short($self->_date, undef, 'I')
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($loc, 0, 50). '...'
+                if $format eq 'latex' && length($loc) > 50;
+              push @d, &{$escape_function}($loc);
+            }
+
+          }
+
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
@@ -3068,18 +4656,27 @@ sub _items_cust_bill_pkg {
 
         }
 
 
         }
 
-        if ( $cust_bill_pkg->recur != 0 &&
-             ( !$type || $type eq 'R' || $type eq 'U' )
+        if (    ( !$type || $type eq 'R' || $type eq 'U' )
+             && (
+                     $cust_bill_pkg->recur != 0
+                  || $cust_bill_pkg->setup == 0
+                  || $discount_show_always
+                  || $cust_bill_pkg->recur_show_zero
+                )
            )
         {
 
            )
         {
 
+          warn "$me _items_cust_bill_pkg adding recur/usage\n"
+            if $DEBUG > 1;
+
           my $is_summary = $display->summary;
           my $is_summary = $display->summary;
-          my $description = $is_summary ? "Usage charges" : $desc;
+          my $description = ($is_summary && $type && $type eq 'U')
+                            ? "Usage charges" : $desc;
 
 
-          unless ( $conf->exists('disable_line_item_date_ranges') ) {
-            $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
-                            " - ". time2str("%x", $cust_bill_pkg->edate). ")";
-          }
+          $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
+                          " - ". time2str($date_format, $cust_bill_pkg->edate).
+                          ")"
+            unless $conf->exists('disable_line_item_date_ranges');
 
           my @d = ();
 
 
           my @d = ();
 
@@ -3088,30 +4685,64 @@ sub _items_cust_bill_pkg {
           my @dates = ( $self->_date );
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
           my @dates = ( $self->_date );
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
+          push @dates, undef 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
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
-                || $is_summary;
+                || $is_summary && $type && $type eq 'U' )
+          {
+
+            warn "$me _items_cust_bill_pkg adding service details\n"
+              if $DEBUG > 1;
+
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short(@dates, 'I')
+                                                   #$cust_bill_pkg->edate,
+                                                   #$cust_bill_pkg->sdate)
+              unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
+
+            warn "$me _items_cust_bill_pkg done adding service details\n"
+              if $DEBUG > 1;
+
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($loc, 0, 50). '...'
+                if $format eq 'latex' && length($loc) > 50;
+              push @d, &{$escape_function}($loc);
+            }
 
 
-          push @d, $cust_bill_pkg->details(%details_opt)
-            unless ($is_summary || $type && $type eq 'R');
+          }
+
+          unless ( $is_summary ) {
+            warn "$me _items_cust_bill_pkg adding details\n"
+              if $DEBUG > 1;
+
+            #instead of omitting details entirely in this case (unwanted side
+            # effects), just omit CDRs
+            $details_opt{'format_function'} = sub { () }
+              if $type && $type eq 'R';
+
+            push @d, $cust_bill_pkg->details(%details_opt);
+          }
+
+          warn "$me _items_cust_bill_pkg calculating amount\n"
+            if $DEBUG > 1;
   
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
   
           my $amount = 0;
           if (!$type) {
             $amount = $cust_bill_pkg->recur;
-          }elsif($type eq 'R') {
+          } elsif ($type eq 'R') {
             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
-          }elsif($type eq 'U') {
+          } elsif ($type eq 'U') {
             $amount = $cust_bill_pkg->usage;
           }
   
           if ( !$type || $type eq 'R' ) {
 
             $amount = $cust_bill_pkg->usage;
           }
   
           if ( !$type || $type eq 'R' ) {
 
+            warn "$me _items_cust_bill_pkg adding recur\n"
+              if $DEBUG > 1;
+
             if ( $cust_bill_pkg->hidden ) {
               $r->{amount}      += $amount;
               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
             if ( $cust_bill_pkg->hidden ) {
               $r->{amount}      += $amount;
               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
@@ -3128,7 +4759,10 @@ sub _items_cust_bill_pkg {
               };
             }
 
               };
             }
 
-          } elsif ( $amount ) {  # && $type eq 'U'
+          } else {  # $type eq 'U'
+
+            warn "$me _items_cust_bill_pkg adding usage\n"
+              if $DEBUG > 1;
 
             if ( $cust_bill_pkg->hidden ) {
               $u->{amount}      += $amount;
 
             if ( $cust_bill_pkg->hidden ) {
               $u->{amount}      += $amount;
@@ -3145,13 +4779,15 @@ sub _items_cust_bill_pkg {
                 ext_description => \@d,
               };
             }
                 ext_description => \@d,
               };
             }
-
           }
 
         } # recurring or usage with recurring charge
 
       } else { #pkgnum tax or one-shot line item (??)
 
           }
 
         } # recurring or usage with recurring charge
 
       } else { #pkgnum tax or one-shot line item (??)
 
+        warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
+          if $DEBUG > 1;
+
         if ( $cust_bill_pkg->setup != 0 ) {
           push @b, {
             'description' => $desc,
         if ( $cust_bill_pkg->setup != 0 ) {
           push @b, {
             'description' => $desc,
@@ -3161,8 +4797,8 @@ sub _items_cust_bill_pkg {
         if ( $cust_bill_pkg->recur != 0 ) {
           push @b, {
             'description' => "$desc (".
         if ( $cust_bill_pkg->recur != 0 ) {
           push @b, {
             'description' => "$desc (".
-                             time2str("%x", $cust_bill_pkg->sdate). ' - '.
-                             time2str("%x", $cust_bill_pkg->edate). ')',
+                             time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+                             time2str($date_format, $cust_bill_pkg->edate). ')',
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
           };
         }
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
           };
         }
@@ -3171,16 +4807,38 @@ sub _items_cust_bill_pkg {
 
     }
 
 
     }
 
-  }
+    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
+                                && $conf->exists('discount-show-always'));
 
 
-  foreach ( $s, $r, $u ) {
-    if ( $_ ) {
-      $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
-      $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-      push @b, { %$_ };
+    foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+      if ( $_ && !$cust_bill_pkg->hidden ) {
+        $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+        $_->{amount}      =~ s/^\-0\.00$/0.00/;
+        $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+        push @b, { %$_ }
+          if $_->{amount} != 0
+          || $discount_show_always
+          || $cust_bill_pkg->recur_show_zero;
+        $_ = undef;
+      }
     }
     }
+
   }
 
   }
 
+  #foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+  #  if ( $_  ) {
+  #    $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+  #    $_->{amount}      =~ s/^\-0\.00$/0.00/;
+  #    $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
+  #    push @b, { %$_ }
+  #      if $_->{amount} != 0
+  #      || $discount_show_always
+  #  }
+  #}
+
+  warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
+    if $DEBUG > 1;
+
   @b;
 
 }
   @b;
 
 }
@@ -3204,7 +4862,7 @@ sub _items_credits {
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => 'Credit applied '.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => 'Credit applied '.
-                       time2str("%x",$_->cust_credit->_date). $reason,
+                       time2str($date_format,$_->cust_credit->_date). $reason,
       'amount'      => sprintf("%.2f",$_->amount),
     };
   }
       'amount'      => sprintf("%.2f",$_->amount),
     };
   }
@@ -3224,7 +4882,7 @@ sub _items_payments {
 
     push @b, {
       'description' => "Payment received ".
 
     push @b, {
       'description' => "Payment received ".
-                       time2str("%x",$_->cust_pay->_date ),
+                       time2str($date_format,$_->cust_pay->_date ),
       'amount'      => sprintf("%.2f", $_->amount )
     };
   }
       'amount'      => sprintf("%.2f", $_->amount )
     };
   }
@@ -3341,7 +4999,7 @@ sub re_X {
   my $distinct = '';
   my $orderby = 'ORDER BY cust_bill._date';
 
   my $distinct = '';
   my $orderby = 'ORDER BY cust_bill._date';
 
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
 
   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
 
   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
@@ -3392,8 +5050,10 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 =cut
 
 sub owed_sql {
 =cut
 
 sub owed_sql {
-  my $class = shift;
-  'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '. 
+    $class->paid_sql($start, $end). ' - '. 
+    $class->credited_sql($start, $end);
 }
 
 =item net_sql
 }
 
 =item net_sql
@@ -3403,8 +5063,8 @@ Returns an SQL fragment to retreive the net amount (charged minus credited).
 =cut
 
 sub net_sql {
 =cut
 
 sub net_sql {
-  my $class = shift;
-  'charged - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '. $class->credited_sql($start, $end);
 }
 
 =item paid_sql
 }
 
 =item paid_sql
@@ -3414,9 +5074,13 @@ Returns an SQL fragment to retreive the amount paid against this invoice.
 =cut
 
 sub paid_sql {
 =cut
 
 sub paid_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_bill_pay._date <= $start";
+  $end   &&= "AND cust_bill_pay._date > $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
-       WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
+       WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
 }
 
 =item credited_sql
 }
 
 =item credited_sql
@@ -3426,25 +5090,44 @@ Returns an SQL fragment to retreive the amount credited against this invoice.
 =cut
 
 sub credited_sql {
 =cut
 
 sub credited_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_credit_bill._date <= $start";
+  $end   &&= "AND cust_credit_bill._date >  $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
-       WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
+       WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
 }
 
 }
 
-=item search_sql HASHREF
+=item due_date_sql
+
+Returns an SQL fragment to retrieve the due date of an invoice.
+Currently only supported on PostgreSQL.
+
+=cut
+
+sub due_date_sql {
+'COALESCE(
+  SUBSTRING(
+    COALESCE(
+      cust_bill.invoice_terms,
+      cust_main.invoice_terms,
+      \''.($conf->config('invoice_default_terms') || '').'\'
+    ), E\'Net (\\\\d+)\'
+  )::INTEGER, 0
+) * 86400 + cust_bill._date'
+}
+
+=item search_sql_where HASHREF
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
 
 =over 4
 
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
 
 =over 4
 
-=item begin
+=item _date
 
 
-Epoch date (UNIX timestamp) setting a lower bound for _date values
-
-=item end
-
-Epoch date (UNIX timestamp) setting an upper bound for _date values
+List reference of start date, end date, as UNIX timestamps.
 
 =item invnum_min
 
 
 =item invnum_min
 
@@ -3452,10 +5135,22 @@ Epoch date (UNIX timestamp) setting an upper bound for _date values
 
 =item agentnum
 
 
 =item agentnum
 
+=item charged
+
+List reference of charged limits (exclusive).
+
 =item owed
 
 =item owed
 
+List reference of charged limits (exclusive).
+
+=item open
+
+flag, return open invoices only
+
 =item net
 
 =item net
 
+flag, return net invoices only
+
 =item days
 
 =item newest_percust
 =item days
 
 =item newest_percust
@@ -3466,40 +5161,68 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
 
 =cut
 
 
 =cut
 
-sub search_sql {
+sub search_sql_where {
   my($class, $param) = @_;
   if ( $DEBUG ) {
   my($class, $param) = @_;
   if ( $DEBUG ) {
-    warn "$me search_sql called with params: \n".
+    warn "$me search_sql_where called with params: \n".
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }
 
   my @search = ();
 
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }
 
   my @search = ();
 
-  if ( $param->{'begin'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill._date >= $1";
+  #agentnum
+  if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
+    push @search, "cust_main.agentnum = $1";
   }
   }
-  if ( $param->{'end'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill._date < $1";
+
+  #_date
+  if ( $param->{_date} ) {
+    my($beginning, $ending) = @{$param->{_date}};
+
+    push @search, "cust_bill._date >= $beginning",
+                  "cust_bill._date <  $ending";
   }
   }
+
+  #invnum
   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
     push @search, "cust_bill.invnum >= $1";
   }
   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
     push @search, "cust_bill.invnum <= $1";
   }
   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
     push @search, "cust_bill.invnum >= $1";
   }
   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
     push @search, "cust_bill.invnum <= $1";
   }
-  if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_main.agentnum = $1";
+
+  #charged
+  if ( $param->{charged} ) {
+    my @charged = ref($param->{charged})
+                    ? @{ $param->{charged} }
+                    : ($param->{charged});
+
+    push @search, map { s/^charged/cust_bill.charged/; $_; }
+                      @charged;
   }
 
   }
 
-  push @search, '0 != '. FS::cust_bill->owed_sql
-    if $param->{'open'};
+  my $owed_sql = FS::cust_bill->owed_sql;
 
 
+  #owed
+  if ( $param->{owed} ) {
+    my @owed = ref($param->{owed})
+                 ? @{ $param->{owed} }
+                 : ($param->{owed});
+    push @search, map { s/^owed/$owed_sql/; $_; }
+                      @owed;
+  }
+
+  #open/net flags
+  push @search, "0 != $owed_sql"
+    if $param->{'open'};
   push @search, '0 != '. FS::cust_bill->net_sql
     if $param->{'net'};
 
   push @search, '0 != '. FS::cust_bill->net_sql
     if $param->{'net'};
 
+  #days
   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
     if $param->{'days'};
 
   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
     if $param->{'days'};
 
+  #newest_percust
   if ( $param->{'newest_percust'} ) {
 
     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
   if ( $param->{'newest_percust'} ) {
 
     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
@@ -3523,6 +5246,7 @@ sub search_sql {
 
   }
 
 
   }
 
+  #agent virtualization
   my $curuser = $FS::CurrentUser::CurrentUser;
   if ( $curuser->username eq 'fs_queue'
        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
   my $curuser = $FS::CurrentUser::CurrentUser;
   if ( $curuser->username eq 'fs_queue'
        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
@@ -3537,7 +5261,6 @@ sub search_sql {
       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
     }
   }
       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
     }
   }
-
   push @search, $curuser->agentnums_sql;
 
   join(' AND ', @search );
   push @search, $curuser->agentnums_sql;
 
   join(' AND ', @search );