delete fees, RT#81713
[freeside.git] / FS / FS / cust_bill.pm
index 35ab9f3..7158cb2 100644 (file)
@@ -1,27 +1,24 @@
 package FS::cust_bill;
 package FS::cust_bill;
+use base qw( FS::cust_bill::Search FS::Template_Mixin
+             FS::cust_main_Mixin FS::Record
+           );
 
 use strict;
 
 use strict;
-use vars qw( @ISA $DEBUG $me 
-             $money_char $date_format $rdate_format $date_format_long );
+use vars qw( $DEBUG $me );
              # but NOT $conf
              # but NOT $conf
-use vars qw( $invoice_lines @buf ); #yuck
+use Carp;
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
-use Date::Language;
-use Text::Template 1.20;
+use DateTime;
 use File::Temp 0.14;
 use File::Temp 0.14;
-use String::ShellQuote;
 use HTML::Entities;
 use HTML::Entities;
-use Locale::Country;
 use Storable qw( freeze thaw );
 use GD::Barcode;
 use FS::UID qw( datasrc );
 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::Misc qw( send_fax do_print );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main_Mixin;
-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;
@@ -31,35 +28,24 @@ use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
-use FS::cust_pay_batch;
-use FS::cust_bill_event;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
-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;
 use FS::discount_plan;
 use FS::payby;
 use FS::bill_batch;
 use FS::cust_bill_batch;
 use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
 use FS::discount_plan;
+use FS::cust_bill_void;
+use FS::reason;
+use FS::reason_type;
 use FS::L10N;
 use FS::L10N;
-
-@ISA = qw( FS::cust_main_Mixin FS::Record );
+use FS::Misc::Savepoint;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
 
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
 
-#ask FS::UID to run this stuff for us later
-FS::UID->install_callback( sub { 
-  my $conf = new FS::Conf; #global
-  $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
 
 FS::cust_bill - Object methods for cust_bill records
 =head1 NAME
 
 FS::cust_bill - Object methods for cust_bill records
@@ -90,7 +76,7 @@ FS::cust_bill - Object methods for cust_bill records
   $tax_amount = $record->tax;
 
   @lines = $cust_bill->print_text;
   $tax_amount = $record->tax;
 
   @lines = $cust_bill->print_text;
-  @lines = $cust_bill->print_text $time;
+  @lines = $cust_bill->print_text('time' => $time);
 
 =head1 DESCRIPTION
 
 
 =head1 DESCRIPTION
 
@@ -116,21 +102,19 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =back
 
 
 =back
 
-Customer info at invoice generation time
+Deprecated fields
 
 =over 4
 
 
 =over 4
 
-=item previous_balance
-
-=item billing_balance
+=item billing_balance - the customer's balance immediately before generating
+this invoice.  DEPRECATED.  Use the L<FS::cust_main/balance_date> method
+to determine the customer's balance at a specific time.
 
 
-=back
-
-Deprecated
-
-=over 4
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one.  DEPRECATED.
 
 
-=item printed - deprecated
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
 
 =back
 
 
 =back
 
@@ -146,6 +130,8 @@ Specific use cases
 
 =item promised_date - customer promised payment date, for collection
 
 
 =item promised_date - customer promised payment date, for collection
 
+=item pending - invoice is still being generated, empty or 'Y'
+
 =back
 
 =head1 METHODS
 =back
 
 =head1 METHODS
@@ -161,8 +147,16 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
 =cut
 
 sub table { 'cust_bill'; }
+sub template_conf { 'invoice_'; }
+
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+  my $self = shift;
+  $self->conf->config('notice_name') || 'Invoice'
+}
 
 
-sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -213,20 +207,84 @@ sub insert {
 
 }
 
 
 }
 
-=item delete
+=item void [ REASON [ , REPROCESS_CDRS ] ]
 
 
-This method now works but you probably shouldn't use it.  Instead, apply a
-credit against the invoice.
+Voids this invoice: deletes the invoice and adds a record of the voided invoice
+to the FS::cust_bill_void table (and related tables starting from
+FS::cust_bill_pkg_void).
 
 
-Using this method to delete invoices outright is really, really bad.  There
-would be no record you ever posted this invoice, and there are no check to
-make sure charged = 0 or that there are no associated cust_bill_pkg records.
+=cut
 
 
-Really, don't use it.
+sub void {
+  my $self = shift;
+  my $reason = scalar(@_) ? shift : '';
+  my $reprocess_cdrs = scalar(@_) ? shift : '';
 
 
-=cut
+  unless (ref($reason) || !$reason) {
+    $reason = FS::reason->new_or_existing(
+      'class'  => 'I',
+      'type'   => 'Invoice void',
+      'reason' => $reason
+    );
+  }
+
+  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 $cust_bill_void = new FS::cust_bill_void ( {
+    map { $_ => $self->get($_) } $self->fields
+  } );
+  $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
+  my $error = $cust_bill_void->insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  $error = $self->_delete;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
+}
+
+# removed docs entirely and renamed method to _delete to further indicate it is
+# internal-only and discourage use
+#
+# =item delete
+#
+# DO NOT USE THIS METHOD.  Instead, apply a credit against the invoice, or use
+# the B<void> method.
+#
+# This is only for internal use by V<void>, which is what you should be using.
+#
+# DO NOT USE THIS METHOD.  Whatever reason you think you have is almost certainly
+# wrong.  Use B<void>, that's what it is for.  Really.  This means you.
+#
+# =cut
 
 
-sub delete {
+sub _delete {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
 
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
 
@@ -242,16 +300,14 @@ sub delete {
   my $dbh = dbh;
 
   foreach my $table (qw(
   my $dbh = dbh;
 
   foreach my $table (qw(
-    cust_bill_event
-    cust_event
-    cust_credit_bill
-    cust_bill_pay
     cust_credit_bill
     cust_credit_bill
-    cust_pay_batch
     cust_bill_pay_batch
     cust_bill_pay_batch
-    cust_bill_pkg
+    cust_bill_pay
     cust_bill_batch
     cust_bill_batch
+    cust_bill_pkg
   )) {
   )) {
+    #cust_event # problematic
+    #cust_pay_batch # unnecessary
 
     foreach my $linked ( $self->$table() ) {
       my $error = $linked->delete;
 
     foreach my $linked ( $self->$table() ) {
       my $error = $linked->delete;
@@ -296,6 +352,7 @@ sub replace_check {
   #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
   #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->pending eq 'Y'
                                     || $old->charged == 0
                                    || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
                                     || $old->charged == 0
                                    || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
@@ -350,6 +407,7 @@ sub check {
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
     || $self->ut_numbern('agent_invid') #varchar?
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
     || $self->ut_numbern('agent_invid') #varchar?
+    || $self->ut_flag('pending')
   ;
   return $error if $error;
 
   ;
   return $error if $error;
 
@@ -369,32 +427,90 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
 
 sub display_invnum {
   my $self = shift;
 
 sub display_invnum {
   my $self = shift;
-  my $conf = $self->conf;
-  if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+  if ( $self->agent_invid
+         && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
     return $self->agent_invid;
   } else {
     return $self->invnum;
   }
 }
 
     return $self->agent_invid;
   } else {
     return $self->invnum;
   }
 }
 
+=item previous_bill
+
+Returns the customer's last invoice before this one.
+
+=cut
+
+sub previous_bill {
+  my $self = shift;
+  if ( !$self->get('previous_bill') ) {
+    $self->set('previous_bill', qsearchs({
+          'table'     => 'cust_bill',
+          'hashref'   => { 'custnum'  => $self->custnum,
+                           '_date'    => { op=>'<', value=>$self->_date } },
+          'order_by'  => 'ORDER BY _date DESC LIMIT 1',
+    }) );
+  }
+  $self->get('previous_bill');
+}
+
+=item following_bill
+
+Returns the customer's invoice that follows this one
+
+=cut
+
+sub following_bill {
+  my $self = shift;
+  if (!$self->get('following_bill')) {
+    $self->set('following_bill', qsearchs({
+      table   => 'cust_bill',
+      hashref => {
+        custnum => $self->custnum,
+        invnum  => { op => '>', value => $self->invnum },
+      },
+      order_by => 'ORDER BY invnum ASC LIMIT 1',
+    }));
+  }
+  $self->get('following_bill');
+}
+
 =item previous
 
 =item previous
 
-Returns a list consisting of the total previous balance for this customer, 
+Returns a list consisting of the total previous balance for this customer,
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
 
 sub previous {
   my $self = shift;
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
 
 sub previous {
   my $self = shift;
-  my $total = 0;
-  my @cust_bill = sort { $a->_date <=> $b->_date }
-    grep { $_->owed != 0 }
-      qsearch( 'cust_bill', { 'custnum' => $self->custnum,
-                              '_date'   => { op=>'<', value=>$self->_date },
-                            } ) 
-  ;
-  foreach ( @cust_bill ) { $total += $_->owed; }
-  $total, @cust_bill;
+  # simple memoize; we use this a lot
+  if (!$self->get('previous')) {
+    my $total = 0;
+    my @cust_bill = sort { $a->_date <=> $b->_date }
+      grep { $_->owed != 0 }
+        qsearch( 'cust_bill', { 'custnum' => $self->custnum,
+                                #'_date'   => { op=>'<', value=>$self->_date },
+                                'invnum'   => { op=>'<', value=>$self->invnum },
+                              } )
+    ;
+    foreach ( @cust_bill ) { $total += $_->owed; }
+    $self->set('previous', [$total, @cust_bill]);
+  }
+  return @{ $self->get('previous') };
+}
+
+=item enable_previous
+
+Whether to show the 'Previous Charges' section when printing this invoice.
+The negation of the 'disable_previous_balance' config setting.
+
+=cut
+
+sub enable_previous {
+  my $self = shift;
+  my $agentnum = $self->cust_main->agentnum;
+  !$self->conf->exists('disable_previous_balance', $agentnum);
 }
 
 =item cust_bill_pkg
 }
 
 =item cust_bill_pkg
@@ -406,9 +522,17 @@ Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
 sub cust_bill_pkg {
   my $self = shift;
   qsearch(
 sub cust_bill_pkg {
   my $self = shift;
   qsearch(
-    { 'table'    => 'cust_bill_pkg',
+    { 
+      'select'    => 'cust_bill_pkg.*, pkg_category.categoryname',
+      'table'    => 'cust_bill_pkg',
+      'addl_from' => ' LEFT JOIN cust_pkg     USING ( pkgnum ) '.
+                     ' LEFT JOIN part_pkg     USING ( pkgpart ) '.
+                     ' LEFT JOIN pkg_class    USING ( classnum ) '.
+                     ' LEFT JOIN pkg_category USING ( categorynum ) ',
       'hashref'  => { 'invnum' => $self->invnum },
       'hashref'  => { 'invnum' => $self->invnum },
-      'order_by' => 'ORDER BY billpkgnum',
+      'order_by' => 'ORDER BY billpkgnum', #important?  otherwise we could use
+                                           # the AUTLOADED FK search.  or should
+                                           # that default to ORDER by the pkey?
     }
   );
 }
     }
   );
 }
@@ -486,32 +610,6 @@ sub open_cust_bill_pkg {
   @open;
 }
 
   @open;
 }
 
-=item cust_bill_event
-
-Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub cust_bill_event {
-  my $self = shift;
-  qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
-}
-
-=item num_cust_bill_event
-
-Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub num_cust_bill_event {
-  my $self = shift;
-  my $sql =
-    "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
-  $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
-  $sth->fetchrow_arrayref->[0];
-}
-
 =item cust_event
 
 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
 =item cust_event
 
 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
@@ -541,7 +639,7 @@ sub num_cust_event {
   my $sql =
     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
   my $sql =
     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
+  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql";
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
@@ -550,11 +648,21 @@ sub num_cust_event {
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
+
+Returns a list: an empty list on success or a list of errors.
+
 =cut
 
 =cut
 
-sub cust_main {
+sub suspend {
   my $self = shift;
   my $self = shift;
-  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  grep { $_->suspend(@_) }
+  grep {! $_->getfield('cancel') }
+  $self->cust_pkg;
+
 }
 
 =item cust_suspend_if_balance_over AMOUNT
 }
 
 =item cust_suspend_if_balance_over AMOUNT
@@ -576,55 +684,35 @@ sub cust_suspend_if_balance_over {
   }
 }
 
   }
 }
 
-=item cust_credit
-
-Depreciated.  See the cust_credited method.
+=item cancel
 
 
- #Returns a list consisting of the total previous credited (see
- #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
- #outstanding credits (FS::cust_credit objects).
+Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
 
 =cut
 
 
 =cut
 
-sub cust_credit {
-  use Carp;
-  croak "FS::cust_bill->cust_credit depreciated; see ".
-        "FS::cust_bill->cust_credit_bill";
-  #my $self = shift;
-  #my $total = 0;
-  #my @cust_credit = sort { $a->_date <=> $b->_date }
-  #  grep { $_->credited != 0 && $_->_date < $self->_date }
-  #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
-  #;
-  #foreach (@cust_credit) { $total += $_->credited; }
-  #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated.  See the cust_bill_pay method.
+sub cancel {
+  my( $self, %opt ) = @_;
 
 
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
+  warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+       join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+    if $DEBUG;
 
 
-=cut
+  return ( 'Access denied' )
+    unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
 
-sub cust_pay {
-  use Carp;
-  croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
-  #my $self = shift;
-  #sort { $a->_date <=> $b->_date }
-  #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
-  #;
-}
+  my @pkgs = $self->cust_pkg;
 
 
-sub cust_pay_batch {
-  my $self = shift;
-  qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
-}
+  if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
+    $opt{nobill} = 1;
+    my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
+    warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+      if $error;
+  }
 
 
-sub cust_bill_pay_batch {
-  my $self = shift;
-  qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+  grep { $_ }
+    map { $_->cancel(%opt) }
+      grep { ! $_->getfield('cancel') }
+        @pkgs;
 }
 
 =item cust_bill_pay
 }
 
 =item cust_bill_pay
@@ -755,7 +843,7 @@ sub cust_bill_batch {
 
 =item discount_plans
 
 
 =item discount_plans
 
-Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
 hash keyed by term length.
 
 =cut
 hash keyed by term length.
 
 =cut
@@ -798,6 +886,35 @@ sub owed {
   $balance;
 }
 
   $balance;
 }
 
+=item owed_on_invoice
+
+Returns the amount to be displayed as the "Balance Due" on this
+invoice.  Amount returned depends on conf flags for invoicing
+
+See L<FS::cust_bill::owed> for the true amount currently owed
+
+=cut
+
+sub owed_on_invoice {
+  my $self = shift;
+
+  #return $self->owed()
+  #  unless $self->conf->exists('previous_balance-payments_since')
+
+  # Add charges from this invoice
+  my $owed = $self->charged();
+
+  # Add carried balances from previous invoices
+  #   If previous items aren't to be displayed on the invoice,
+  #   _items_previous() is aware of this and responds appropriately.
+  $owed += $_->{amount} for $self->_items_previous();
+
+  # Subtract payments and credits displayed on this invoice
+  $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
+
+  return $owed;
+}
+
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
@@ -833,6 +950,7 @@ sub hide {
 =item apply_payments_and_credits [ OPTION => VALUE ... ]
 
 Applies unapplied payments and credits to this invoice.
 =item apply_payments_and_credits [ OPTION => VALUE ... ]
 
 Applies unapplied payments and credits to this invoice.
+Payments with the no_auto_apply flag set will not be applied.
 
 A hash of optional arguments may be passed.  Currently "manual" is supported.
 If true, a payment receipt is sent instead of a statement when
 
 A hash of optional arguments may be passed.  Currently "manual" is supported.
 If true, a payment receipt is sent instead of a statement when
@@ -857,9 +975,14 @@ sub apply_payments_and_credits {
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
   local $FS::UID::AutoCommit = 0;
   my $dbh = dbh;
 
+  my $savepoint_label = 'cust_bill__apply_payments_and_credits';
+  savepoint_create( $savepoint_label );
+
   $self->select_for_update; #mutex
 
   $self->select_for_update; #mutex
 
-  my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
+  my @payments = grep { $_->unapplied > 0 }
+                   grep { !$_->no_auto_apply }
+                     $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
   if ( $conf->exists('pkg-balances') ) {
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
   if ( $conf->exists('pkg-balances') ) {
@@ -886,7 +1009,7 @@ sub apply_payments_and_credits {
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
-            grep { $_ } 
+            grep { $_ }
              map  { $_->cust_pkg }
                   @open_lineitems
            );
              map  { $_->cust_pkg }
                   @open_lineitems
            );
@@ -897,7 +1020,7 @@ sub apply_payments_and_credits {
       } else {
         $app = 'credit';
       }
       } else {
         $app = 'credit';
       }
-    
+
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
@@ -943,6 +1066,7 @@ sub apply_payments_and_credits {
 
     my $error = $app->insert(%options);
     if ( $error ) {
 
     my $error = $app->insert(%options);
     if ( $error ) {
+      savepoint_rollback_and_release( $savepoint_label );
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
     }
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
     }
@@ -950,593 +1074,204 @@ sub apply_payments_and_credits {
 
   }
 
 
   }
 
+  savepoint_release( $savepoint_label );
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
 }
 
   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
   ''; #no error
 
 }
 
-=item generate_email OPTION => VALUE ...
-
-Options:
-
-=over 4
-
-=item from
-
-sender address, required
-
-=item tempate
-
-alternate template name, optional
+=item send HASHREF
 
 
-=item print_text
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
 
-text attachment arrayref, optional
+Options can be passed as a hashref.  Positional parameters are no longer
+allowed.
 
 
-=item subject
+I<template>: a suffix for alternate invoices
 
 
-email subject, optional
+I<agentnum>: obsolete, now does nothing.
 
 
-=item notice_name
+I<from> overrides the default email invoice From: address.
 
 
-notice name instead of "Invoice", optional
+I<amount>: obsolete, does nothing
 
 
-=back
+I<notice_name> overrides "Invoice" as the name of the sent document
+(templates from 10/2009 or newer required).
 
 
-Returns an argument list to be passed to L<FS::Misc::send_email>.
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
 
 =cut
 
 
 =cut
 
-use MIME::Entity;
-
-sub generate_email {
-
+sub send {
   my $self = shift;
   my $self = shift;
-  my %args = @_;
+  my $opt = ref($_[0]) ? $_[0] : +{ @_ };
   my $conf = $self->conf;
 
   my $conf = $self->conf;
 
-  my $me = '[FS::cust_bill::generate_email]';
-
-  my %return = (
-    'from'      => $args{'from'},
-    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
-  );
-
-  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;
 
   my $cust_main = $self->cust_main;
 
-  if (ref($args{'to'}) eq 'ARRAY') {
-    $return{'to'} = $args{'to'};
-  } else {
-    $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
-                           $cust_main->invoicing_list
-                    ];
-  }
-
-  if ( $conf->exists('invoice_html') ) {
-
-    warn "$me creating HTML/text multipart message"
-      if $DEBUG;
-
-    $return{'nobody'} = 1;
+  my @invoicing_list = $cust_main->invoicing_list;
 
 
-    my $alternative = build MIME::Entity
-      'Type'        => 'multipart/alternative',
-      #'Encoding'    => '7bit',
-      'Disposition' => 'inline'
-    ;
+  $self->email($opt)
+    if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+    && ! $cust_main->invoice_noemail;
 
 
-    my $data;
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
+  $self->print($opt)
+    if grep { $_ eq 'POST' } @invoicing_list; #postal
 
 
-      warn "$me using 'invoice_email_pdf_note' in multipart message"
-        if $DEBUG;
-      $data = [ map { $_ . "\n" }
-                    $conf->config('invoice_email_pdf_note')
-              ];
+  #this has never been used post-$ORIGINAL_ISP afaik
+  $self->fax_invoice($opt)
+    if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
 
-    } else {
+  '';
 
 
-      warn "$me not using 'invoice_email_pdf_note' in multipart message"
-        if $DEBUG;
-      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
-        $data = $args{'print_text'};
-      } else {
-        $data = [ $self->print_text(\%opt) ];
-      }
+}
 
 
-    }
+sub email {
+  my $self = shift;
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die ref($self). '->email called with positional parameters';
+  }
 
 
-    $alternative->attach(
-      'Type'        => 'text/plain',
-      'Encoding'    => 'quoted-printable',
-      #'Encoding'    => '7bit',
-      'Data'        => $data,
-      'Disposition' => 'inline',
-    );
+  my $conf = $self->conf;
 
 
+  my $from = delete $opt->{from};
 
 
-    my $htmldata;
-    my $image = '';
-    my $barcode = '';
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
+  # this is where we set the From: address
+  $from ||= $self->_agent_invoice_from ||    #XXX should go away
+            $conf->invoice_from_full( $self->cust_main->agentnum );
 
 
-      $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
+  my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
 
 
+  if ( ! @invoicing_list ) { #no recipients
+    if ( $conf->exists('cust_bill-no_recipients-error') ) {
+      die 'No recipients for customer #'. $self->custnum;
     } else {
     } else {
-
-      $args{'from'} =~ /\@([\w\.\-]+)/;
-      my $from = $1 || 'example.com';
-      my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-
-      my $logo;
-      my $agentnum = $cust_main->agentnum;
-      if ( defined($args{'template'}) && length($args{'template'})
-           && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
-         )
-      {
-        $logo = 'logo_'. $args{'template'}. '.png';
-      } else {
-        $logo = "logo.png";
-      }
-      my $image_data = $conf->config_binary( $logo, $agentnum);
-
-      $image = build MIME::Entity
-        'Type'       => 'image/png',
-        'Encoding'   => 'base64',
-        'Data'       => $image_data,
-        'Filename'   => 'logo.png',
-        'Content-ID' => "<$content_id>",
-      ;
-   
-      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;
-      }
-
-      $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
-    }
-
-    $alternative->attach(
-      'Type'        => 'text/html',
-      'Encoding'    => 'quoted-printable',
-      'Data'        => [ '<html>',
-                         '  <head>',
-                         '    <title>',
-                         '      '. encode_entities($return{'subject'}), 
-                         '    </title>',
-                         '  </head>',
-                         '  <body bgcolor="#e8e8e8">',
-                         $htmldata,
-                         '  </body>',
-                         '</html>',
-                       ],
-      'Disposition' => 'inline',
-      #'Filename'    => 'invoice.pdf',
-    );
-
-
-    my @otherparts = ();
-    if ( $cust_main->email_csv_cdr ) {
-
-      push @otherparts, build MIME::Entity
-        'Type'        => 'text/csv',
-        'Encoding'    => '7bit',
-        'Data'        => [ map { "$_\n" }
-                             $self->call_details('prepend_billed_number' => 1)
-                         ],
-        'Disposition' => 'attachment',
-        'Filename'    => 'usage-'. $self->invnum. '.csv',
-      ;
-
+      #default: better to notify this person than silence
+      @invoicing_list = ($from);
     }
     }
+  }
 
 
-    if ( $conf->exists('invoice_email_pdf') ) {
-
-      #attaching pdf too:
-      # multipart/mixed
-      #   multipart/related
-      #     multipart/alternative
-      #       text/plain
-      #       text/html
-      #     image/png
-      #   application/pdf
-
-      my $related = build MIME::Entity 'Type'     => 'multipart/related',
-                                       'Encoding' => '7bit';
-
-      #false laziness w/Misc::send_email
-      $related->head->replace('Content-type',
-        $related->mime_type.
-        '; boundary="'. $related->head->multipart_boundary. '"'.
-        '; type=multipart/alternative'
-      );
-
-      $related->add_part($alternative);
+  $self->SUPER::email( {
+    'from' => $from,
+    'to'   => \@invoicing_list,
+    %$opt,
+  });
 
 
-      $related->add_part($image) if $image;
+}
 
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
+#this stays here for now because its explicitly used as
+# FS::cust_bill::queueable_email
+sub queueable_email {
+  my %opt = @_;
 
 
-      $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
+  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
+    or die "invalid invoice number: " . $opt{invnum};
 
 
-    } else {
+  $self->set('mode', $opt{mode})
+    if $opt{mode};
 
 
-      #no other attachment:
-      # multipart/related
-      #   multipart/alternative
-      #     text/plain
-      #     text/html
-      #   image/png
+  my %args = map {$_ => $opt{$_}}
+             grep { $opt{$_} }
+              qw( from notice_name no_coupon template );
 
 
-      $return{'content-type'} = 'multipart/related';
-      if ($conf->exists('invoice-barcode') && $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';
+  my $error = $self->email( \%args );
+  die $error if $error;
 
 
-    }
-  
-  } else {
+}
 
 
-    if ( $conf->exists('invoice_email_pdf') ) {
-      warn "$me creating PDF attachment"
-        if $DEBUG;
+sub email_subject {
+  my $self = shift;
+  my $conf = $self->conf;
 
 
-      #mime parts arguments a la MIME::Entity->build().
-      $return{'mimeparts'} = [
-        { $self->mimebuild_pdf(\%opt) }
-      ];
-    }
-  
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
+  #my $template = scalar(@_) ? shift : '';
+  #per-template?
 
 
-      warn "$me using 'invoice_email_pdf_note'"
-        if $DEBUG;
-      $return{'body'} = [ map { $_ . "\n" }
-                              $conf->config('invoice_email_pdf_note')
-                        ];
+  my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+                || 'Invoice';
 
 
-    } else {
+  my $cust_main = $self->cust_main;
+  my $name = $cust_main->name;
+  my $name_short = $cust_main->name_short;
+  my $invoice_number = $self->invnum;
+  my $invoice_date = $self->_date_pretty;
 
 
-      warn "$me not using 'invoice_email_pdf_note'"
-        if $DEBUG;
-      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
-        $return{'body'} = $args{'print_text'};
-      } else {
-        $return{'body'} = [ $self->print_text(\%opt) ];
-      }
+  eval qq("$subject");
+}
 
 
-    }
+sub pdf_filename {
+  my $self = shift;
+  'Invoice-'. $self->invnum. '.pdf';
+}
 
 
-  }
+=item lpr_data HASHREF
 
 
-  %return;
+Returns the postscript or plaintext for this invoice as an arrayref.
 
 
-}
+Options must be passed as a hashref.  Positional parameters are no longer
+allowed.
 
 
-=item mimebuild_pdf
+I<template>, if specified, is the name of a suffix for alternate invoices.
 
 
-Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 
 =cut
 
-sub mimebuild_pdf {
+sub lpr_data {
   my $self = shift;
   my $self = shift;
-  (
-    'Type'        => 'application/pdf',
-    'Encoding'    => 'base64',
-    'Data'        => [ $self->print_pdf(@_) ],
-    'Disposition' => 'attachment',
-    'Filename'    => 'invoice-'. $self->invnum. '.pdf',
-  );
+  my $conf = $self->conf;
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    # nobody does this anyway
+    die "FS::cust_bill::lpr_data called with positional parameters";
+  }
+
+  my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
+  [ $self->$method( $opt ) ];
 }
 
 }
 
-=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
+=item print HASHREF
 
 
-Sends this invoice to the destinations configured for this customer: sends
-email, prints and/or faxes.  See L<FS::cust_main_invoice>.
+Prints this invoice.
 
 
-Options can be passed as a hashref (recommended) or as a list of up to 
-four values for templatename, agentnum, invoice_from and amount.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
 
 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.
-
-I<invoice_from>, if specified, overrides the default email invoice From: address.
-
-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.
-
 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 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 {
-  my %opt = @_;
-
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my @args = ( $opt{template}, $opt{agentnum} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
-
-  my $error = $self->send( @args );
-  die $error if $error;
-
-}
-
-sub send {
-  my $self = shift;
-  my $conf = $self->conf;
-
-  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 $cust_main = $self->cust_main;
-
-  return 'N/A' unless ! $agentnums
-                   or grep { $_ == $cust_main->agentnum } @$agentnums;
-
-  return ''
-    unless $cust_main->total_owed_date($self->_date) > $balance_over;
-
-  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
-                    $conf->config('invoice_from', $cust_main->agentnum );
-
-  my %opt = (
-    'template'     => $template,
-    'invoice_from' => $invoice_from,
-    'notice_name'  => ( $notice_name || 'Invoice' ),
-  );
-
-  my @invoicing_list = $cust_main->invoicing_list;
-
-  #$self->email_invoice(\%opt)
-  $self->email(\%opt)
-    if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
-    && ! $self->invoice_noemail;
-
-  #$self->print_invoice(\%opt)
-  $self->print(\%opt)
-    if grep { $_ eq 'POST' } @invoicing_list; #postal
-
-  $self->fax_invoice(\%opt)
-    if grep { $_ eq 'FAX' } @invoicing_list; #fax
-
-  '';
-
-}
-
-=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
-
-Emails this invoice.
-
-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.
-
-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
-
-sub queueable_email {
-  my %opt = @_;
-
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my %args = ( 'template' => $opt{template} );
-  $args{$_} = $opt{$_}
-    foreach grep { exists($opt{$_}) && $opt{$_} }
-              qw( invoice_from notice_name no_coupon );
-
-  my $error = $self->email( \%args );
-  die $error if $error;
-
-}
-
-#sub email_invoice {
-sub email {
-  my $self = shift;
-  return if $self->hide;
-  my $conf = $self->conf;
-
-  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;
-
-  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(
-      '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 "$error\n" if $error;
-
-}
-
-sub email_subject {
-  my $self = shift;
-  my $conf = $self->conf;
-
-  #my $template = scalar(@_) ? shift : '';
-  #per-template?
-
-  my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
-                || 'Invoice';
-
-  my $cust_main = $self->cust_main;
-  my $name = $cust_main->name;
-  my $name_short = $cust_main->name_short;
-  my $invoice_number = $self->invnum;
-  my $invoice_date = $self->_date_pretty;
-
-  eval qq("$subject");
-}
-
-=item lpr_data HASHREF | [ TEMPLATE ]
-
-Returns the postscript or plaintext for this invoice as an arrayref.
-
-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 {
-  my $self = shift;
-  my $conf = $self->conf;
-  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 HASHREF | [ TEMPLATE ]
-
-Prints this invoice.
-
-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;
   return if $self->hide;
   my $conf = $self->conf;
 sub print {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  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 = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::print called with positional parameters";
   }
 
   }
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
+  my $lpr = delete $opt->{lpr};
   if($conf->exists('invoice_print_pdf')) {
     # Add the invoice to the current batch.
   if($conf->exists('invoice_print_pdf')) {
     # Add the invoice to the current batch.
-    $self->batch_invoice(\%opt);
+    $self->batch_invoice($opt);
   }
   else {
   }
   else {
-    do_print $self->lpr_data(\%opt);
+    do_print(
+      $self->lpr_data($opt),
+      'agentnum' => $self->cust_main->agentnum,
+      'lpr'      => $lpr,
+    );
   }
 }
 
   }
 }
 
-=item fax_invoice HASHREF | [ TEMPLATE ] 
+=item fax_invoice HASHREF
 
 Faxes this invoice.
 
 
 Faxes this invoice.
 
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1548,15 +1283,9 @@ sub fax_invoice {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  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 = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::fax_invoice called with positional parameters";
   }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
   }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
@@ -1565,12 +1294,7 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
-  my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
+  my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
@@ -1579,9 +1303,11 @@ sub fax_invoice {
 
 =item batch_invoice [ HASHREF ]
 
 
 =item batch_invoice [ HASHREF ]
 
-Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there
 isn't an open batch, one will be created.
 
 isn't an open batch, one will be created.
 
+HASHREF may contain any options to be passed to C<print_pdf>.
+
 =cut
 
 sub batch_invoice {
 =cut
 
 sub batch_invoice {
@@ -1591,6 +1317,10 @@ sub batch_invoice {
       batchnum => $bill_batch->batchnum,
       invnum   => $self->invnum,
   });
       batchnum => $bill_batch->batchnum,
       invnum   => $self->invnum,
   });
+  if ( $self->mode ) {
+    $opt->{mode} ||= $self->mode;
+    $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
+  }
   return $cust_bill_batch->insert($opt);
 }
 
   return $cust_bill_batch->insert($opt);
 }
 
@@ -1617,7 +1347,7 @@ sub get_open_bill_batch {
   return $batch;
 }
 
   return $batch;
 }
 
-=item ftp_invoice [ TEMPLATENAME ] 
+=item ftp_invoice [ TEMPLATENAME ]
 
 Sends this invoice data via FTP.
 
 
 Sends this invoice data via FTP.
 
@@ -1640,7 +1370,7 @@ sub ftp_invoice {
   );
 }
 
   );
 }
 
-=item spool_invoice [ TEMPLATENAME ] 
+=item spool_invoice [ TEMPLATENAME ]
 
 Spools this invoice data (see L<FS::spool_csv>)
 
 
 Spools this invoice data (see L<FS::spool_csv>)
 
@@ -1659,29 +1389,6 @@ sub spool_invoice {
   );
 }
 
   );
 }
 
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
-  my $self = shift;
-
-  return ''
-    if scalar(
-               grep { $_->owed > 0 } 
-                    qsearch('cust_bill', {
-                      'custnum' => $self->custnum,
-                      #'_date'   => { op=>'>', value=>$self->_date },
-                      'invnum'  => { op=>'>', value=>$self->invnum },
-                    } )
-             );
-    
-  $self->send(@_);
-}
-
 =item send_csv OPTION => VALUE, ...
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
 =item send_csv OPTION => VALUE, ...
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
@@ -1704,14 +1411,20 @@ See L</print_csv> for a description of the output format.
 sub send_csv {
   my($self, %opt) = @_;
 
 sub send_csv {
   my($self, %opt) = @_;
 
+  if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+    warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+    return;
+  }
+
   #create file(s)
 
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
   #create file(s)
 
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
+  # don't localize dates here, they're a defined format
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
-  
+
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
@@ -1762,13 +1475,16 @@ L<FS::cust_main_invoice>).
 =item agent_spools - if set to a true value, will spool to per-agent files
 rather than a single global file
 
 =item agent_spools - if set to a true value, will spool to per-agent files
 rather than a single global file
 
-=item ftp_targetnum - if set to an FTP target (see L<FS::ftp_target>), will
+=item upload_targetnum - if set to a target (see L<FS::upload_target>), will
 append to that spool.  L<FS::Cron::upload> will then send the spool file to
 that destination.
 
 =item balanceover - if set, only spools the invoice if the total amount owed on
 this invoice and all older invoices is greater than the specified amount.
 
 append to that spool.  L<FS::Cron::upload> will then send the spool file to
 that destination.
 
 =item balanceover - if set, only spools the invoice if the total amount owed on
 this invoice and all older invoices is greater than the specified amount.
 
+=item time - the "current time".  Controls the printing of past due messages
+in the ICS format.
+
 =back
 
 =cut
 =back
 
 =cut
@@ -1776,6 +1492,12 @@ this invoice and all older invoices is greater than the specified amount.
 sub spool_csv {
   my($self, %opt) = @_;
 
 sub spool_csv {
   my($self, %opt) = @_;
 
+  if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+    warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+    return;
+  }
+
+  my $time = $opt{'time'} || time;
   my $cust_main = $self->cust_main;
 
   if ( $opt{'dest'} ) {
   my $cust_main = $self->cust_main;
 
   if ( $opt{'dest'} ) {
@@ -1793,7 +1515,7 @@ sub spool_csv {
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
-  my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
+  my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
 
   my $file;
   if ( $opt{'agent_spools'} ) {
 
   my $file;
   if ( $opt{'agent_spools'} ) {
@@ -1802,8 +1524,8 @@ sub spool_csv {
     $file = 'spool';
   }
 
     $file = 'spool';
   }
 
-  if ( $opt{'ftp_targetnum'} ) {
-    $spooldir .= '/target'.$opt{'ftp_targetnum'};
+  if ( $opt{'upload_targetnum'} ) {
+    $spooldir .= '/target'.$opt{'upload_targetnum'};
     mkdir $spooldir, 0700 unless -d $spooldir;
   } # otherwise it just goes into export.xxx/cust_bill
 
     mkdir $spooldir, 0700 unless -d $spooldir;
   } # otherwise it just goes into export.xxx/cust_bill
 
@@ -1812,8 +1534,8 @@ sub spool_csv {
   }
 
   $file = "$spooldir/$file.csv";
   }
 
   $file = "$spooldir/$file.csv";
-  
-  my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
+
+  my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
   flock(CSV, LOCK_EX);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
   flock(CSV, LOCK_EX);
@@ -1833,7 +1555,7 @@ sub spool_csv {
     seek(CSV, 0, 2);
   }
 
     seek(CSV, 0, 2);
   }
 
-  print CSV $detail;
+  print CSV $detail if defined($detail);
 
   flock(CSV, LOCK_UN);
   close CSV;
 
   flock(CSV, LOCK_UN);
   close CSV;
@@ -1857,7 +1579,7 @@ detail information for this invoice.
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
-record_type, invnum, custnum, _date, charged, first, last, company, address1, 
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
@@ -1963,57 +1685,71 @@ If I<format> is "billco", the fields of the detail CSV file are as follows:
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
-If format is 'oneline', there is no detail file.  Each invoice has a 
+If format is 'oneline', there is no detail file.  Each invoice has a
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
 line 1, address line 2, city, state, zip, invoice date, invoice number,
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
 line 1, address line 2, city, state, zip, invoice date, invoice number,
-amount charged, amount due,
+amount charged, amount due, previous balance, due date.
 
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
 
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
-If format is 'bridgestone', there is no detail file.  Each invoice has a 
+If format is 'bridgestone', there is no detail file.  Each invoice has a
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
-sending redundant notices, the spooling event should have a "once" or 
+sending redundant notices, the spooling event should have a "once" or
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
-  
+
   eval "use Text::CSV_XS";
   die $@ if $@;
 
   my $cust_main = $self->cust_main;
 
   my $csv = Text::CSV_XS->new({'always_quote'=>1});
   eval "use Text::CSV_XS";
   die $@ if $@;
 
   my $cust_main = $self->cust_main;
 
   my $csv = Text::CSV_XS->new({'always_quote'=>1});
+  my $format = lc($opt{'format'});
 
 
-  if ( lc($opt{'format'}) eq 'billco' ) {
+  my $time = $opt{'time'} || time;
+
+  $self->set('_template', $opt{template})
+    if exists $opt{template};
+
+  my $tracctnum = ''; #leaking out from billco-specific sections :/
+  if ( $format eq 'billco' ) {
+
+    my $account_num =
+      $self->conf->config('billco-account_num', $cust_main->agentnum);
+
+    $tracctnum = $account_num eq 'display_custnum'
+                   ? $cust_main->display_custnum
+                   : $opt{'tracctnum'};
 
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
 
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
-    my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+    my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
-      foreach ( $self->_items_payments, $self->_items_credits ) ;
+      foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
     $csv->combine(
       '',                         #  1 | N/A-Leave Empty               CHAR   2
       '',                         #  2 | N/A-Leave Empty               CHAR  15
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
     $csv->combine(
       '',                         #  1 | N/A-Leave Empty               CHAR   2
       '',                         #  2 | N/A-Leave Empty               CHAR  15
-      $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
+      $tracctnum,                 #  3 | Transaction Account No        CHAR  15
       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
@@ -2048,15 +1784,19 @@ sub print_csv {
       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
     );
 
       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
     );
 
-  } elsif ( lc($opt{'format'}) eq 'oneline' ) { #name?
-  
-    my ($previous_balance) = $self->previous; 
+  } elsif ( $format eq 'oneline' ) { #name
+
+    my ($previous_balance) = $self->previous;
+    $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
-      ($_->{pkgnum} || ''),
-      $_->{description},
-      $_->{amount}
-    } $self->_items_pkg;
+                      $_->{pkgnum},
+                      $_->{description},
+                      $_->{amount}
+                    }
+                  $self->_items_pkg, #_items_nontax?  no sections or anything
+                                     # with this format
+                  $self->_items_tax;
 
     $csv->combine(
       $cust_main->agentnum,
 
     $csv->combine(
       $cust_main->agentnum,
@@ -2064,6 +1804,7 @@ sub print_csv {
       $self->custnum,
       $cust_main->first,
       $cust_main->last,
       $self->custnum,
       $cust_main->first,
       $cust_main->last,
+      $cust_main->company,
       $cust_main->address1,
       $cust_main->address2,
       $cust_main->city,
       $cust_main->address1,
       $cust_main->address2,
       $cust_main->city,
@@ -2075,14 +1816,16 @@ sub print_csv {
       $self->invnum,
       $self->charged,
       $totaldue,
       $self->invnum,
       $self->charged,
       $totaldue,
+      $previous_balance,
+      $self->due_date2str("%x"),
 
       @items,
     );
 
 
       @items,
     );
 
-  } elsif ( lc($opt{'format'}) eq 'bridgestone' ) {
+  } elsif ( $format eq 'bridgestone' ) {
 
     # bypass the CSV stuff and just return this
 
     # bypass the CSV stuff and just return this
-    my $longdate = time2str('%B %d, %Y', time); #current time, right?
+    my $longdate = time2str('%B %d, %Y', $time); #current time, right?
     my $zip = $cust_main->zip;
     $zip =~ s/\D//;
     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
     my $zip = $cust_main->zip;
     $zip =~ s/\D//;
     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
@@ -2104,8 +1847,122 @@ sub print_csv {
       '' #detail
       );
 
       '' #detail
       );
 
-  } else {
-  
+  } elsif ( $format eq 'ics' ) {
+
+    my $bill = $cust_main->bill_location;
+    my $zip = $bill->zip;
+    my $zip4 = '';
+
+    $zip =~ s/\D//;
+    if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
+      $zip = $1;
+      $zip4 = $2;
+    }
+
+    # minor false laziness with print_generic
+    my ($previous_balance) = $self->previous;
+    my $balance_due = $self->owed + $previous_balance;
+    my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
+    my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
+
+    my $past_due = '';
+    if ( $self->due_date and $time >= $self->due_date ) {
+      $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
+    }
+
+    # again, bypass CSV
+    my $header = sprintf(
+      '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
+      $cust_main->display_custnum, #BID
+      uc($cust_main->first), #FNAME
+      uc($cust_main->last), #LNAME
+      '00', #BATCH, should this ever be anything else?
+      uc($cust_main->company), #COMP
+      uc($bill->address1), #STREET1
+      uc($bill->address2), #STREET2
+      uc($bill->city), #CITY
+      uc($bill->state), #STATE
+      $zip,
+      $zip4,
+      time2str('%Y%m%d', $self->_date), #BILL_DATE
+      $self->due_date2str('%Y%m%d'), #DUE_DATE,
+      ( map {sprintf('%0.2f', $_)}
+        $balance_due, #AMNT_DUE
+        $previous_balance, #PREV_BAL
+        $payment_total, #PYMT_RCVD
+        $credit_total, #CREDITS
+        $previous_balance, #BEG_BAL--is this correct?
+        $self->charged, #NEW_CHRG
+      ),
+      'img01', #MRKT_MSG?
+      $past_due, #PAST_MSG
+    );
+
+    my @details;
+    my %svc_class = ('' => ''); # maybe cache this more persistently?
+
+    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+
+      my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
+      my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
+
+      if ( $cust_pkg ) {
+
+        my @dates = ( $self->_date, undef );
+        if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
+          $dates[1] = $prev->sdate; #questionable
+        }
+
+        # generate an 01 detail for each service
+        my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
+        foreach my $cust_svc ( @svcs ) {
+          $show_pkgnum = ''; # hide it if we're showing svcnums
+
+          my $svcpart = $cust_svc->svcpart;
+          if (!exists($svc_class{$svcpart})) {
+            my $classnum = $cust_svc->part_svc->classnum;
+            my $part_svc_class = FS::part_svc_class->by_key($classnum)
+              if $classnum;
+            $svc_class{$svcpart} = $part_svc_class ?
+                                   $part_svc_class->classname :
+                                   '';
+          }
+
+          my @h_label = $cust_svc->label(@dates, 'I');
+          push @details, sprintf('01%-9s%-20s%-47s',
+            $cust_svc->svcnum,
+            $svc_class{$svcpart},
+            $h_label[1],
+          );
+        } #foreach $cust_svc
+      } #if $cust_pkg
+
+      my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
+      if ($cust_bill_pkg->recur > 0) {
+        $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
+                     time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
+      }
+      push @details, sprintf('02%-6s%-60s%-10s',
+        $show_pkgnum,
+        $desc,
+        sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
+      );
+    } #foreach $cust_bill_pkg
+
+    # Tag this row so that we know whether this is one page (1), two pages
+    # (2), # or "big" (B).  The tag will be stripped off before uploading.
+    if ( scalar(@details) < 12 ) {
+      push @details, '1';
+    } elsif ( scalar(@details) < 58 ) {
+      push @details, '2';
+    } else {
+      push @details, 'B';
+    }
+
+    return join('', $header, @details, "\n");
+
+  } else { # default
+
     $csv->combine(
       'cust_bill',
       $self->invnum,
     $csv->combine(
       'cust_bill',
       $self->invnum,
@@ -2124,15 +1981,27 @@ sub print_csv {
   if ( lc($opt{'format'}) eq 'billco' ) {
 
     my $lineseq = 0;
   if ( lc($opt{'format'}) eq 'billco' ) {
 
     my $lineseq = 0;
-    foreach my $item ( $self->_items_pkg ) {
+    my %items_opt = ( format => 'template',
+                      escape_function => sub { shift } );
+    # I don't know what characters billco actually tolerates in spool entries.
+    # Text::CSV will take care of delimiters, though.
+
+    my @items = ( $self->_items_pkg(%items_opt),
+                  $self->_items_fee(%items_opt) );
+    foreach my $item (@items) {
+
+      my $description = $item->{'description'};
+      if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
+        $description .= ': ' . $item->{ext_description}[0];
+      }
 
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
 
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
-        $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
+        $tracctnum,             #  3 | Account Number             CHAR  15
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
-        $item->{'description'}, #  6 | Transaction Detail         CHAR 100
+        $description,           #  6 | Transaction Detail         CHAR 100
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
@@ -2153,7 +2022,7 @@ sub print_csv {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
-      
+
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
@@ -2162,21 +2031,21 @@ sub print_csv {
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
-          ( $cust_bill_pkg->sdate 
+          ( $cust_bill_pkg->sdate
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
-          ($cust_bill_pkg->edate 
-            ?time2str("%x", $cust_bill_pkg->edate)
+          ($cust_bill_pkg->edate
+            ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
             : '' ),
         );
-  
+
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
-  
+
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
@@ -2194,24 +2063,8 @@ sub print_csv {
 
 }
 
 
 }
 
-=item comp
-
-Pays this invoice with a compliemntary payment.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
 sub comp {
 sub comp {
-  my $self = shift;
-  my $cust_pay = new FS::cust_pay ( {
-    'invnum'   => $self->invnum,
-    'paid'     => $self->owed,
-    '_date'    => '',
-    'payby'    => 'COMP',
-    'payinfo'  => $self->cust_main->payinfo,
-    'paybatch' => '',
-  } );
-  $cust_pay->insert;
+  croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
 }
 
 =item realtime_card
 }
 
 =item realtime_card
@@ -2311,7 +2164,7 @@ sub batch_card {
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
-  
+
   $cust_main->batch_card(%options);
 }
 
   $cust_main->batch_card(%options);
 }
 
@@ -2325,1970 +2178,53 @@ sub _agent_invoice_from {
   $self->cust_main->agent_invoice_from;
 }
 
   $self->cust_main->agent_invoice_from;
 }
 
-=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
-
-Returns an text invoice, as a list of lines.
-
-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.
-
-I<template>, if specified, is the name of a suffix for alternate invoices.
+=item invoice_barcode DIR_OR_FALSE
 
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+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
 
 
 =cut
 
-sub print_text {
-  my $self = shift;
-  my( $today, $template, %opt );
-  if ( ref($_[0]) ) {
-    %opt = %{ shift() };
-    $today = delete($opt{'time'}) || '';
-    $template = delete($opt{template}) || '';
-  } else {
-    ( $today, $template, %opt ) = @_;
-  }
+sub invoice_barcode {
+    my ($self, $dir) = (shift,shift);
 
 
-  my %params = ( 'format' => 'template' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
+    my $gdbar = new GD::Barcode('Code39',$self->invnum);
+       die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
+    my $gd = $gdbar->plot(Height => 30);
 
 
-  $self->print_generic( %params );
+    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_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
-an associated logo (with the .eps extension included).
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-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.
+=item invnum_date_pretty
 
 
-I<template>, if specified, is the name of a suffix for alternate invoices.
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)".
 
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+Intended for back-end context, with regard to translation and date formatting.
 
 =cut
 
 
 =cut
 
-sub print_latex {
+#note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
+# for backend use (and also does the wrong thing, localizing for end customer
+# instead of backoffice configured date format)
+sub invnum_date_pretty {
   my $self = shift;
   my $self = shift;
-  my $conf = $self->conf;
-  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;
-  $params{$_} = $opt{$_} 
-    foreach grep $opt{$_}, qw( unsquelch_cdr notice_name );
-
-  $template ||= $self->_agent_template;
-
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
-                           DIR      => $dir,
-                           SUFFIX   => '.eps',
-                           UNLINK   => 0,
-                         ) or die "can't open temp file: $!\n";
-
-  my $agentnum = $self->cust_main->agentnum;
-
-  if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
-    print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
-      or die "can't write temp file: $!\n";
-  } else {
-    print $lh $conf->config_binary('logo.eps', $agentnum)
-      or die "can't write temp file: $!\n";
-  }
-  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',
-                           DIR      => $dir,
-                           SUFFIX   => '.tex',
-                           UNLINK   => 0,
-                         ) or die "can't open temp file: $!\n";
-  binmode($fh, ':utf8'); # language support
-  print $fh join('', @filled_in );
-  close $fh;
-
-  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
-  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 OPTION => VALUE ...
-
-Internal method - returns a filled-in template for this invoice as a scalar.
-
-See print_ps and print_pdf for methods that return PostScript and PDF output.
-
-Non optional options include 
-  format - latex, html, template
-
-Optional options include
-
-template - a value used as a suffix for a configuration template
-
-time - a 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.
-
-cid - 
-
-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)
-
-locale - override customer's locale
-
-=cut
-
-#what's with all the sprintf('%10.2f')'s in here?  will it cause any
-# (alignment in text invoice?) problems to change them all to '%.2f' ?
-# yes: fixed width/plain text printing will be borked
-sub print_generic {
-  my( $self, %params ) = @_;
-  my $conf = $self->conf;
-  my $today = $params{today} ? $params{today} : time;
-  warn "$me print_generic called on $self with suffix $params{template}\n"
-    if $DEBUG;
-
-  my $format = $params{format};
-  die "Unknown format: $format"
-    unless $format =~ /^(latex|html|template)$/;
-
-  my $cust_main = $self->cust_main;
-  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname
-        && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
-
-  my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
-                     'html'     => [ '<%=', '%>' ],
-                     '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"
-    if length($template) && $conf->exists($templatefile."_$template");
-  my @invoice_template = map "$_\n", $conf->config($templatefile)
-    or die "cannot load config data $templatefile";
-
-  my $old_latex = '';
-  if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
-    #change this to a die when the old code is removed
-    warn "old-style invoice template $templatefile; ".
-         "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
-    $old_latex = 'true';
-    @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},
-  );
-
-  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";
-
-
-  # additional substitution could possibly cause breakage in existing templates
-  my %convert_maps = ( 
-    'latex' => {
-                 'notes'         => sub { map "$_", @_ },
-                 'footer'        => sub { map "$_", @_ },
-                 'smallfooter'   => sub { map "$_", @_ },
-                 'returnaddress' => sub { map "$_", @_ },
-                 'coupon'        => sub { map "$_", @_ },
-                 'summary'       => sub { map "$_", @_ },
-               },
-    'html'  => {
-                 'notes' =>
-                   sub {
-                     map { 
-                       s/%%(.*)$/<!-- $1 -->/g;
-                       s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
-                       s/\\begin\{enumerate\}/<ol>/g;
-                       s/\\item /  <li>/g;
-                       s/\\end\{enumerate\}/<\/ol>/g;
-                       s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
-                       s/\\\\\*/<br>/g;
-                       s/\\dollar ?/\$/g;
-                       s/\\#/#/g;
-                       s/~/&nbsp;/g;
-                       $_;
-                     }  @_
-                   },
-                 'footer' =>
-                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
-                 'smallfooter' =>
-                   sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
-                 'returnaddress' =>
-                   sub {
-                     map { 
-                       s/~/&nbsp;/g;
-                       s/\\\\\*?\s*$/<BR>/;
-                       s/\\hyphenation\{[\w\s\-]+}//;
-                       s/\\([&])/$1/g;
-                       $_;
-                     }  @_
-                   },
-                 'coupon'        => sub { "" },
-                 'summary'       => sub { "" },
-               },
-    'template' => {
-                 'notes' =>
-                   sub {
-                     map { 
-                       s/%%.*$//g;
-                       s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
-                       s/\\begin\{enumerate\}//g;
-                       s/\\item /  * /g;
-                       s/\\end\{enumerate\}//g;
-                       s/\\textbf\{(.*)\}/$1/g;
-                       s/\\\\\*/ /;
-                       s/\\dollar ?/\$/g;
-                       $_;
-                     }  @_
-                   },
-                 'footer' =>
-                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
-                 'smallfooter' =>
-                   sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
-                 'returnaddress' =>
-                   sub {
-                     map { 
-                       s/~/ /g;
-                       s/\\\\\*?\s*$/\n/;             # dubious
-                       s/\\hyphenation\{[\w\s\-]+}//;
-                       $_;
-                     }  @_
-                   },
-                 'coupon'        => sub { "" },
-                 'summary'       => sub { "" },
-               },
-  );
-
-
-  # hashes for differing output formats
-  my %nbsps = ( 'latex'    => '~',
-                'html'     => '',    # '&nbps;' would be nice
-                'template' => '',    # not used
-              );
-  my $nbsp = $nbsps{$format};
-
-  my %escape_functions = ( 'latex'    => \&_latex_escape,
-                           'html'     => \&_html_escape_nbsp,#\&encode_entities,
-                           'template' => sub { shift },
-                         );
-  my $escape_function = $escape_functions{$format};
-  my $escape_function_nonbsp = ($format eq 'html')
-                                 ? \&_html_escape : $escape_function;
-
-  my %date_formats = ( 'latex'    => $date_format_long,
-                       'html'     => $date_format_long,
-                       'template' => '%s',
-                     );
-  $date_formats{'html'} =~ s/ /&nbsp;/g;
-
-  my $date_format = $date_formats{$format};
-
-  my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
-                                               },
-                             'html'     => sub { return '<b>'. shift(). '</b>'
-                                               },
-                             'template' => sub { shift },
-                           );
-  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;
-  if (
-         defined( $conf->config_orbase( "invoice_${format}returnaddress",
-                                        $template
-                                      )
-                )
-       && length( $conf->config_orbase( "invoice_${format}returnaddress",
-                                        $template
-                                      )
-                )
-  ) {
-
-    $returnaddress = join("\n",
-      $conf->config_orbase("invoice_${format}returnaddress", $template)
-    );
-
-  } elsif ( grep /\S/,
-            $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
-
-    my $convert_map = $convert_maps{$format}{'returnaddress'};
-    $returnaddress =
-      join( "\n",
-            &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
-                                                 $template
-                                               )
-                         )
-          );
-  } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
-
-    my $convert_map = $convert_maps{$format}{'returnaddress'};
-    $returnaddress = join( "\n", &$convert_map(
-                                   map { s/( {2,})/'~' x length($1)/eg;
-                                         s/$/\\\\\*/;
-                                         $_
-                                       }
-                                     ( $conf->config('company_name', $self->cust_main->agentnum),
-                                       $conf->config('company_address', $self->cust_main->agentnum),
-                                     )
-                                 )
-                     );
-
-  } else {
-
-    my $warning = "Couldn't find a return address; ".
-                  "do you need to set the company_address configuration value?";
-    warn "$warning\n";
-    $returnaddress = $nbsp;
-    #$returnaddress = $warning;
-
-  }
-
-  warn "$me generating invoice data\n"
-    if $DEBUG > 1;
-
-  my $agentnum = $self->cust_main->agentnum;
-
-  my %invoice_data = (
-
-    #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),
-    'today'           => time2str($date_format_long, $today),
-    'terms'           => $self->terms,
-    'template'        => $template, #params{'template'},
-    'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
-    'current_charges' => sprintf("%.2f", $self->charged),
-    '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'),
-    '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,
-
-  );
-  #localization
-  my $lh = FS::L10N->get_handle( $params{'locale'} || $cust_main->locale );
-  $invoice_data{'emt'} = sub { &$escape_function($self->mt(@_)) };
-  my %info = FS::Locales->locale_info($cust_main->locale || 'en_US');
-  # eval to avoid death for unimplemented languages
-  my $dh = eval { Date::Language->new($info{'name'}) } ||
-           Date::Language->new(); # fall back to English
-  # prototype here to silence warnings
-  $invoice_data{'time2str'} = sub ($;$$) { $dh->time2str(@_) };
-  # eventually use this date handle everywhere in here, too
-
-  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';
-  foreach ( qw( address1 address2 city state zip country fax) ){
-    my $method = 'ship_'.$_;
-    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
-  }
-  foreach ( qw( contact company ) ) { #compatibility
-    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$_);
-  }
-  $invoice_data{'ship_country'} = ''
-    if ( $invoice_data{'ship_country'} eq $countrydefault );
-  
-  $invoice_data{'cid'} = $params{'cid'}
-    if $params{'cid'};
-
-  if ( $cust_main->country eq $countrydefault ) {
-    $invoice_data{'country'} = '';
-  } else {
-    $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
-  }
-
-  my @address = ();
-  $invoice_data{'address'} = \@address;
-  push @address,
-    $cust_main->payname.
-      ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
-        ? " (P.O. #". $cust_main->payinfo. ")"
-        : ''
-      )
-  ;
-  push @address, $cust_main->company
-    if $cust_main->company;
-  push @address, $cust_main->address1;
-  push @address, $cust_main->address2
-    if $cust_main->address2;
-  push @address,
-    $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
-  push @address, $invoice_data{'country'}
-    if $invoice_data{'country'};
-  push @address, ''
-    while (scalar(@address) < 5);
-
-  $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;
-
-  # the customer's current balance as shown on the invoice before this one
-  $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
-
-  # the change in balance from that invoice to this one
-  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
-
-  # the sum of amount owed on all previous invoices
-  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
-
-  # the sum of amount owed on all invoices
-  $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
-
-  # info from customer's last invoice before this one, for some 
-  # summary formats
-  $invoice_data{'last_bill'} = {};
-  my $last_bill = $pr_cust_bill[-1];
-  if ( $last_bill ) {
-    $invoice_data{'last_bill'} = {
-      '_date'     => $last_bill->_date, #unformatted
-      # all we need for now
-    };
-  }
-
-  my $summarypage = '';
-  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
-    $summarypage = 1;
-  }
-  $invoice_data{'summarypage'} = $summarypage;
-
-  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;
-
-    if ( $conf->exists($inc_file, $agentnum)
-         && 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');
-  }
-
-  # let invoices use either of these as needed
-  $invoice_data{'po_num'} = ($cust_main->payby eq 'BILL') 
-    ? $cust_main->payinfo : '';
-  $invoice_data{'po_line'} = 
-    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
-      ? &$escape_function($self->mt("Purchase Order #").$cust_main->payinfo)
-      : $nbsp;
-
-  my %money_chars = ( 'latex'    => '',
-                      'html'     => $conf->config('money_char') || '$',
-                      'template' => '',
-                    );
-  my $money_char = $money_chars{$format};
-
-  my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
-                            'html'     => $conf->config('money_char') || '$',
-                            'template' => '',
-                          );
-  my $other_money_char = $other_money_chars{$format};
-  $invoice_data{'dollar'} = $other_money_char;
-
-  my @detail_items = ();
-  my @total_items = ();
-  my @buf = ();
-  my @sections = ();
-
-  $invoice_data{'detail_items'} = \@detail_items;
-  $invoice_data{'total_items'} = \@total_items;
-  $invoice_data{'buf'} = \@buf;
-  $invoice_data{'sections'} = \@sections;
-
-  warn "$me generating sections\n"
-    if $DEBUG > 1;
-
-  my $previous_section = { 'description' => $self->mt('Previous Charges'),
-                           'subtotal'    => $other_money_char.
-                                            sprintf('%.2f', $pr_total),
-                           'summarized'  => '', #why? $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' => $self->mt('Taxes, Surcharges, and Fees'),
-                      'subtotal'    => $taxtotal,   # adjusted below
-                    };
-  my $tax_weight = _pkg_category($tax_section->{description})
-                        ? _pkg_category($tax_section->{description})->weight
-                        : 0;
-  $tax_section->{'summarized'} = ''; #why? $summarypage && !$tax_weight ? 'Y' : '';
-  $tax_section->{'sort_weight'} = $tax_weight;
-
-
-  my $adjusttotal = 0;
-  my $adjust_section = { 'description' => 
-    $self->mt('Credits, Payments, and Adjustments'),
-                         'subtotal'    => 0,   # adjusted below
-                       };
-  my $adjust_weight = _pkg_category($adjust_section->{description})
-                        ? _pkg_category($adjust_section->{description})->weight
-                        : 0;
-  $adjust_section->{'summarized'} = ''; #why? $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);
-  $invoice_data{'multisection'} = $multisection;
-  my $late_sections = [];
-  my $extra_sections = [];
-  my $extra_lines = ();
-
-  my $default_section = { 'description' => '',
-                          'subtotal'    => '', 
-                          'no_subtotal' => 1,
-                        };
-
-  if ( $multisection ) {
-    ($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 {# not multisection
-    # make a default section
-    push @sections, $default_section;
-    # and calculate the finance charge total, since it won't get done otherwise.
-    # XXX possibly other totals?
-    # XXX possibly finance_pkgclass should not be used in this manner?
-    if ( $conf->exists('finance_pkgclass') ) {
-      my @finance_charges;
-      foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-        if ( grep { $_->section eq $invoice_data{finance_section} }
-             $cust_bill_pkg->cust_bill_pkg_display ) {
-          # I think these are always setup fees, but just to be sure...
-          push @finance_charges, $cust_bill_pkg->recur + $cust_bill_pkg->setup;
-        }
-      }
-      $invoice_data{finance_amount} = 
-        sprintf('%.2f', sum( @finance_charges ) || 0);
-    }
-  }
-
-  unless (    $conf->exists('disable_previous_balance', $agentnum)
-           || $conf->exists('previous_balance-summary_only')
-         )
-  {
-
-    warn "$me adding previous balances\n"
-      if $DEBUG > 1;
-
-    foreach my $line_item ( $self->_items_previous ) {
-
-      my $detail = {
-        ext_description => [],
-      };
-      $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = 1;
-      $detail->{'section'} = $multisection ? $previous_section
-                                           : $default_section;
-      $detail->{'description'} = &$escape_function($line_item->{'description'});
-      if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = map {
-          &$escape_function($_);
-        } @{$line_item->{'ext_description'}};
-      }
-      $detail->{'amount'} = ( $old_latex ? '' : $money_char).
-                            $line_item->{'amount'};
-      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
-      push @detail_items, $detail;
-      push @buf, [ $detail->{'description'},
-                   $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-                 ];
-    }
-
-  }
-  
-  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance', $agentnum) ) 
-    {
-    push @buf, ['','-----------'];
-    push @buf, [ $self->mt('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) {
-
-    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;
-
-    # continue some normalization
-    $section->{'amount'}   = $section->{'subtotal'}
-      if $multisection;
-
-
-    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{'no_usage'} = 1 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) ) {
-
-      warn "$me     adding line item $line_item\n"
-        if $DEBUG > 1;
-
-      my $detail = {
-        ext_description => [],
-      };
-      $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = $line_item->{'quantity'};
-      $detail->{'section'} = $section;
-      $detail->{'description'} = &$escape_function($line_item->{'description'});
-      if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
-      }
-      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
-                              $line_item->{'amount'};
-      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
-                                 $line_item->{'unit_amount'};
-      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-
-      $detail->{'sdate'} = $line_item->{'sdate'};
-      $detail->{'edate'} = $line_item->{'edate'};
-      $detail->{'seconds'} = $line_item->{'seconds'};
-  
-      push @detail_items, $detail;
-      push @buf, ( [ $detail->{'description'},
-                     $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-                   ],
-                   map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
-                 );
-    }
-
-    if ( $section->{'description'} ) {
-      push @buf, ( ['','-----------'],
-                   [ $section->{'description'}. ' sub-total',
-                      $section->{'subtotal'} # already formatted this 
-                   ],
-                   [ '', '' ],
-                   [ '', '' ],
-                 );
-    }
-  
-  }
-
-  $invoice_data{current_less_finance} =
-    sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
-
-  if ( $multisection && !$conf->exists('disable_previous_balance', $agentnum)
-    || $conf->exists('previous_balance-summary_only') )
-  {
-    unshift @sections, $previous_section if $pr_total;
-  }
-
-  warn "$me adding taxes\n"
-    if $DEBUG > 1;
-
-  foreach my $tax ( $self->_items_tax ) {
-
-    $taxtotal += $tax->{'amount'};
-
-    my $description = &$escape_function( $tax->{'description'} );
-    my $amount      = sprintf( '%.2f', $tax->{'amount'} );
-
-    if ( $multisection ) {
-
-      my $money = $old_latex ? '' : $money_char;
-      push @detail_items, {
-        ext_description => [],
-        ref          => '',
-        quantity     => '',
-        description  => $description,
-        amount       => $money. $amount,
-        product_code => '',
-        section      => $tax_section,
-      };
-
-    } else {
-
-      push @total_items, {
-        'total_item'   => $description,
-        'total_amount' => $other_money_char. $amount,
-      };
-
-    }
-
-    push @buf,[ $description,
-                $money_char. $amount,
-              ];
-
-  }
-  
-  if ( $taxtotal ) {
-    my $total = {};
-    $total->{'total_item'} = $self->mt('Sub-total');
-    $total->{'total_amount'} =
-      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-
-    if ( $multisection ) {
-      $tax_section->{'subtotal'} = $other_money_char.
-                                   sprintf('%.2f', $taxtotal);
-      $tax_section->{'pretotal'} = 'New charges sub-total '.
-                                   $total->{'total_amount'};
-      push @sections, $tax_section if $taxtotal;
-    }else{
-      unshift @total_items, $total;
-    }
-  }
-  $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
-
-  push @buf,['','-----------'];
-  push @buf,[$self->mt( 
-              $conf->exists('disable_previous_balance', $agentnum) 
-               ? 'Total Charges'
-               : 'Total New Charges'
-             ),
-             $money_char. sprintf("%10.2f",$self->charged) ];
-  push @buf,['',''];
-
-  {
-    my $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', $agentnum) ||
-                     $conf->exists('previous_balance-exclude_from_total')
-                     ? 0
-                     : $pr_total
-                   );
-    $total->{'total_item'} = &$embolden_function($self->mt($item));
-    $total->{'total_amount'} =
-      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
-    if ( $multisection ) {
-      if ( $adjust_section->{'sort_weight'} ) {
-        $adjust_section->{'posttotal'} = $self->mt('Balance Forward').' '.
-          $other_money_char.  sprintf("%.2f", ($self->billing_balance || 0) );
-      } else {
-        $adjust_section->{'pretotal'} = $self->mt('New charges total').' '.
-          $other_money_char.  sprintf('%.2f', $self->charged );
-      } 
-    }else{
-      push @total_items, $total;
-    }
-    push @buf,['','-----------'];
-    push @buf,[$item,
-               $money_char.
-               sprintf( '%10.2f', $amount )
-              ];
-    push @buf,['',''];
-  }
-  
-  unless ( $conf->exists('disable_previous_balance', $agentnum) ) {
-    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-  
-    # credits
-    my $credittotal = 0;
-    foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
-
-      my $total;
-      $total->{'total_item'} = &$escape_function($credit->{'description'});
-      $credittotal += $credit->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
-      $adjusttotal += $credit->{'amount'};
-      if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($credit->{'description'}),
-          amount       => $money. $credit->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      } else {
-        push @total_items, $total;
-      }
-
-    }
-    $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
-
-    #credits (again)
-    foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
-      push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
-    }
-
-    # payments
-    my $paymenttotal = 0;
-    foreach my $payment ( $self->_items_payments ) {
-      my $total = {};
-      $total->{'total_item'} = &$escape_function($payment->{'description'});
-      $paymenttotal += $payment->{'amount'};
-      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
-      $adjusttotal += $payment->{'amount'};
-      if ( $multisection ) {
-        my $money = $old_latex ? '' : $money_char;
-        push @detail_items, {
-          ext_description => [],
-          ref          => '',
-          quantity     => '',
-          description  => &$escape_function($payment->{'description'}),
-          amount       => $money. $payment->{'amount'},
-          product_code => '',
-          section      => $adjust_section,
-        };
-      }else{
-        push @total_items, $total;
-      }
-      push @buf, [ $payment->{'description'},
-                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
-                 ];
-    }
-    $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
-  
-    if ( $multisection ) {
-      $adjust_section->{'subtotal'} = $other_money_char.
-                                      sprintf('%.2f', $adjusttotal);
-      push @sections, $adjust_section
-        unless $adjust_section->{sort_weight};
-    }
-
-    # create Balance Due message
-    { 
-      my $total;
-      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-      $total->{'total_amount'} =
-        &$embolden_function(
-          $other_money_char. sprintf('%.2f', $summarypage 
-                                               ? $self->charged +
-                                                 $self->billing_balance
-                                               : $self->owed + $pr_total
-                                    )
-        );
-      if ( $multisection && !$adjust_section->{sort_weight} ) {
-        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
-                                         $total->{'total_amount'};
-      }else{
-        push @total_items, $total;
-      }
-      push @buf,['','-----------'];
-      push @buf,[$self->balance_due_msg, $money_char. 
-        sprintf("%10.2f", $balance_due ) ];
-    }
-
-    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 ($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;
-  }
-
-  # make a discounts-available section, even without multisection
-  if ( $conf->exists('discount-show_available') 
-       and my @discounts_avail = $self->_items_discounts_avail ) {
-    my $discount_section = {
-      'description' => $self->mt('Discounts Available'),
-      'subtotal'    => '',
-      'no_subtotal' => 1,
-    };
-
-    push @sections, $discount_section;
-    push @detail_items, map { +{
-        'ref'         => '', #should this be something else?
-        'section'     => $discount_section,
-        'description' => &$escape_function( $_->{description} ),
-        'amount'      => $money_char . &$escape_function( $_->{amount} ),
-        'ext_description' => [ &$escape_function($_->{ext_description}) || () ],
-    } } @discounts_avail;
-  }
-
-  # All sections and items are built; now fill in templates.
-  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\((\d*)\)/;
-    $invoice_lines += $1 || scalar(@buf);
-    $wasfunc=1;
-  }
-  die "no invoice_lines() functions in template?"
-    if ( $format eq 'template' && !$wasfunc );
-
-  if ($format eq 'template') {
-
-    if ( $invoice_lines ) {
-      $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
-      $invoice_data{'total_pages'}++
-        if scalar(@buf) % $invoice_lines;
-    }
-
-    #setup subroutine for the template
-    $invoice_data{invoice_lines} = sub {
-      my $lines = shift || scalar(@buf);
-      map { 
-        scalar(@buf)
-          ? shift @buf
-          : [ '', '' ];
-      }
-      ( 1 .. $lines );
-    };
-
-    my $lines;
-    my @collect;
-    while (@buf) {
-      push @collect, split("\n",
-        $text_template->fill_in( HASH => \%invoice_data )
-      );
-      $invoice_data{'page'}++;
-    }
-    map "$_\n", @collect;
-  }else{
-    # this is where we actually create the invoice
-    warn "filling in template for invoice ". $self->invnum. "\n"
-      if $DEBUG;
-    warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
-      if $DEBUG > 1;
-
-    $text_template->fill_in(HASH => \%invoice_data);
-  }
-}
-
-# 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.
-
-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.
-
-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;
-
-  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
-  my $ps = generate_ps($file);
-  unlink($logofile);
-  unlink($barcodefile) if $barcodefile;
-
-  $ps;
-}
-
-=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
-
-Returns an PDF invoice, as a scalar.
-
-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.
-
-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;
-
-  my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
-  my $pdf = generate_pdf($file);
-  unlink($logofile);
-  unlink($barcodefile) if $barcodefile;
-
-  $pdf;
-}
-
-=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
-
-Returns an HTML invoice, as a scalar.
-
-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.
-
-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
-
-sub print_html {
-  my $self = shift;
-  my %params;
-  if ( ref($_[0]) ) {
-    %params = %{ shift() }; 
-  }else{
-    $params{'time'} = shift;
-    $params{'template'} = shift;
-    $params{'cid'} = shift;
-  }
-
-  $params{'format'} = 'html';
-  
-  $self->print_generic( %params );
-}
-
-# quick subroutine for print_latex
-#
-# There are ten characters that LaTeX treats as special characters, which
-# means that they do not simply typeset themselves: 
-#      # $ % & ~ _ ^ \ { }
-#
-# TeX ignores blanks following an escaped character; if you want a blank (as
-# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
-
-sub _latex_escape {
-  my $value = shift;
-  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
-  $value =~ s/([<>])/\$$1\$/g;
-  $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 {
-  warn "_translate_old_latex_format called\n"
-    if $DEBUG; 
-
-  my @template = ();
-  while ( @_ ) {
-    my $line = shift;
-  
-    if ( $line =~ /^%%Detail\s*$/ ) {
-  
-      push @template, q![@--!,
-                      q!  foreach my $_tr_line (@detail_items) {!,
-                      q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
-                      q!      $_tr_line->{'description'} .= !, 
-                      q!        "\\tabularnewline\n~~".!,
-                      q!        join( "\\tabularnewline\n~~",!,
-                      q!          @{$_tr_line->{'ext_description'}}!,
-                      q!        );!,
-                      q!    }!;
-
-      while ( ( my $line_item_line = shift )
-              !~ /^%%EndDetail\s*$/                            ) {
-        $line_item_line =~ s/'/\\'/g;    # nice LTS
-        $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
-        $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
-        push @template, "    \$OUT .= '$line_item_line';";
-      }
-
-      push @template, '}',
-                      '--@]';
-      #' doh, gvim
-    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
-
-      push @template, '[@--',
-                      '  foreach my $_tr_line (@total_items) {';
-
-      while ( ( my $total_item_line = shift )
-              !~ /^%%EndTotalDetails\s*$/                      ) {
-        $total_item_line =~ s/'/\\'/g;    # nice LTS
-        $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
-        $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
-        push @template, "    \$OUT .= '$total_item_line';";
-      }
-
-      push @template, '}',
-                      '--@]';
-
-    } else {
-      $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
-      push @template, $line;  
-    }
-  
-  }
-
-  if ($DEBUG) {
-    warn "$_\n" foreach @template;
-  }
-
-  (@template);
-}
-
-sub terms {
-  my $self = shift;
-  my $conf = $self->conf;
-
-  #check for an invoice-specific override
-  return $self->invoice_terms if $self->invoice_terms;
-  
-  #check for a customer- specific override
-  my $cust_main = $self->cust_main;
-  return $cust_main->invoice_terms if $cust_main->invoice_terms;
-
-  #use configured default
-  $conf->config('invoice_default_terms') || '';
-}
-
-sub due_date {
-  my $self = shift;
-  my $duedate = '';
-  if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = $self->_date() + ( $1 * 86400 );
-  }
-  $duedate;
-}
-
-sub due_date2str {
-  my $self = shift;
-  $self->due_date ? time2str(shift, $self->due_date) : '';
-}
-
-sub balance_due_msg {
-  my $self = shift;
-  my $msg = $self->mt('Balance Due');
-  return $msg unless $self->terms;
-  if ( $self->due_date ) {
-    $msg .= ' - ' . $self->mt('Please pay by'). ' '.
-      $self->due_date2str($date_format);
-  } elsif ( $self->terms ) {
-    $msg .= ' - '. $self->terms;
-  }
-  $msg;
-}
-
-sub balance_due_date {
-  my $self = shift;
-  my $conf = $self->conf;
-  my $duedate = '';
-  if (    $conf->exists('invoice_default_terms') 
-       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
-  }
-  $duedate;
-}
-
-sub credit_balance_msg { 
-  my $self = shift;
-  $self->mt('Credit Balance Remaining')
-}
-
-=item invnum_date_pretty
-
-Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
-
-=cut
-
-sub invnum_date_pretty {
-  my $self = shift;
-  $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
-}
-
-=item _date_pretty
-
-Returns a string with the date, for example: "3/20/2008"
-
-=cut
-
-sub _date_pretty {
-  my $self = shift;
-  time2str($date_format, $self->_date);
-}
-
-=item _items_sections LATE SUMMARYPAGE ESCAPE EXTRA_SECTIONS FORMAT
-
-Generate section information for all items appearing on this invoice.
-This will only be called for multi-section invoices.
-
-For each line item (L<FS::cust_bill_pkg> record), this will fetch all 
-related display records (L<FS::cust_bill_pkg_display>) and organize 
-them into two groups ("early" and "late" according to whether they come 
-before or after the total), then into sections.  A subtotal is calculated 
-for each section.
-
-Section descriptions are returned in sort weight order.  Each consists 
-of a hash containing:
-
-description: the package category name, escaped
-subtotal: the total charges in that section
-tax_section: a flag indicating that the section contains only tax charges
-summarized: same as tax_section, for some reason
-sort_weight: the package category's sort weight
-
-If 'condense' is set on the display record, it also contains everything 
-returned from C<_condense_section()>, i.e. C<_condensed_foo_generator>
-coderefs to generate parts of the invoice.  This is not advised.
-
-Arguments:
-
-LATE: an arrayref to push the "late" section hashes onto.  The "early"
-group is simply returned from the method.
-
-SUMMARYPAGE: a flag indicating whether this is a summary-format invoice.
-Turning this on has the following effects:
-- Ignores display items with the 'summary' flag.
-- Combines all items into the "early" group.
-- Creates sections for all non-disabled package categories, even if they 
-have no charges on this invoice, as well as a section with no name.
-
-ESCAPE: an escape function to use for section titles.
-
-EXTRA_SECTIONS: an arrayref of additional sections to return after the 
-sorted list.  If there are any of these, section subtotals exclude 
-usage charges.
-
-FORMAT: 'latex', 'html', or 'template' (i.e. text).  Not used, but 
-passed through to C<_condense_section()>.
-
-=cut
-
-use vars qw(%pkg_category_cache);
-sub _items_sections {
-  my $self = shift;
-  my $late = shift;
-  my $summarypage = shift;
-  my $escape = shift;
-  my $extra_sections = shift;
-  my $format = shift;
-
-  my %subtotal = ();
-  my %late_subtotal = ();
-  my %not_tax = ();
-
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
-  {
-
-      my $usage = $cust_bill_pkg->usage;
-
-      foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
-        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 && !$summarypage ) {
-          if (! $type || $type eq 'S') {
-            $late_subtotal{$section} += $cust_bill_pkg->setup
-              if $cust_bill_pkg->setup != 0
-              || $cust_bill_pkg->setup_show_zero;
-          }
-
-          if (! $type) {
-            $late_subtotal{$section} += $cust_bill_pkg->recur
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-
-          if ($type && $type eq 'R') {
-            $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-          
-          if ($type && $type eq 'U') {
-            $late_subtotal{$section} += $usage
-              unless scalar(@$extra_sections);
-          }
-
-        } else {
-
-          next if $cust_bill_pkg->pkgnum == 0 && ! $section;
-
-          if (! $type || $type eq 'S') {
-            $subtotal{$section} += $cust_bill_pkg->setup
-              if $cust_bill_pkg->setup != 0
-              || $cust_bill_pkg->setup_show_zero;
-          }
-
-          if (! $type) {
-            $subtotal{$section} += $cust_bill_pkg->recur
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-
-          if ($type && $type eq 'R') {
-            $subtotal{$section} += $cust_bill_pkg->recur - $usage
-              if $cust_bill_pkg->recur != 0
-              || $cust_bill_pkg->recur_show_zero;
-          }
-          
-          if ($type && $type eq 'U') {
-            $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
-  }
-
-  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)
-                  );
-    }
-
-    $prefix. join( $separator, @result ). $suffix;
-  };
-
-}
+  #$self->mt('Invoice #').
+  'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+    $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
+}
 
 #sub _items_extra_usage_sections {
 #  my $self = shift;
 
 #sub _items_extra_usage_sections {
 #  my $self = shift;
@@ -4334,7 +2270,7 @@ sub _items_extra_usage_sections {
   my %classnums = ();
   my %lines = ();
 
   my %classnums = ();
   my %lines = ();
 
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
@@ -4347,13 +2283,13 @@ sub _items_extra_usage_sections {
       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
         my $amount = $detail->amount;
         next unless $amount && $amount > 0;
       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;
 
         $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 $desc = $detail->regionname;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
@@ -4395,7 +2331,7 @@ sub _items_extra_usage_sections {
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -4442,31 +2378,32 @@ sub _did_summary {
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            my $phone_deleted;
            $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            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
 # 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
                    || $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
                    || $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
+                 local($FS::Record::qsearch_qualify_columns) = 0;
                 my $phone_portedin = qsearchs( '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),  
+                     { '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
                    );
                 $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
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
-           } 
-           elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
-               && $phone_deleted->lnp_status 
+           }
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+               && $phone_deleted->lnp_status
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
@@ -4519,8 +2456,8 @@ sub _items_accountcode_cdr {
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
-                if($detail->detail eq $section->{'header'}); 
-      
+                if($detail->detail eq $section->{'header'});
+
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
@@ -4574,7 +2511,7 @@ sub _items_svc_phone_sections {
   my %classnums = ();
   my %lines = ();
 
   my %classnums = ();
   my %lines = ();
 
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
@@ -4603,7 +2540,7 @@ sub _items_svc_phone_sections {
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
-      my $desc = $detail->regionname; 
+      my $desc = $detail->regionname;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
@@ -4692,7 +2629,7 @@ sub _items_svc_phone_sections {
                                 total_line_generator
                               )
                           )
                                 total_line_generator
                               )
                           )
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -4711,8 +2648,8 @@ sub _items_svc_phone_sections {
       push @lines, $l;
     }
   }
       push @lines, $l;
     }
   }
-  
-  if($conf->exists('phone_usage_class_summary')) { 
+
+  if($conf->exists('phone_usage_class_summary')) {
       # this only works with Latex
       my @newlines;
       my @newsections;
       # this only works with Latex
       my @newlines;
       my @newsections;
@@ -4738,7 +2675,7 @@ sub _items_svc_phone_sections {
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
-           push @newsections, \%calls_detail;  
+           push @newsections, \%calls_detail;
        }
       }
 
        }
       }
 
@@ -4747,7 +2684,7 @@ sub _items_svc_phone_sections {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
-               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
@@ -4768,643 +2705,870 @@ sub _items_svc_phone_sections {
        }
       }
 
        }
       }
 
-      # 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;
-           }
-       }
+      # 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);
+
+}
+
+=item _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this
+invoice.  Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
+  my $self = shift;
+  my %opt = @_;
+
+  my $escape = $opt{escape} || sub { $_[0] };
+  my $money_char = $opt{money_char};
+  my $invnum = $self->invnum;
+  my @classes = qsearch({
+      'table'     => 'usage_class',
+      'select'    => 'classnum, classname, SUM(amount) AS amount,'.
+                     ' COUNT(*) AS calls, SUM(duration) AS duration',
+      'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
+                     ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
+      'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
+                     ' GROUP BY classnum, classname, weight'.
+                     ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
+                     ' ORDER BY weight ASC',
+  });
+  my @l;
+  my $section = {
+    description   => &{$escape}($self->mt('Usage Summary')),
+    usage_section => 1,
+    subtotal      => 0,
+  };
+  foreach my $class (@classes) {
+    $section->{subtotal} += $class->get('amount');
+    push @l, {
+      'description'     => &{$escape}($class->classname),
+      'amount'          => $money_char.sprintf('%.2f', $class->get('amount')),
+      'quantity'        => $class->get('calls'),
+      'duration'        => $class->get('duration'),
+      'usage_classnum'  => $class->classnum,
+      'section'         => $section,
+    };
+  }
+  $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
+  return @l;
+}
+
+=item _items_previous()
+
+  Returns an array of hashrefs, each hashref representing a line-item on
+  the current bill for previous unpaid invoices.
+
+  keys for each previous_item:
+  - amount (see notes)
+  - pkgnum
+  - description
+  - invnum
+  - _date
+
+  Payments and credits shown on this invoice may vary based on configuraiton.
+
+  when conf flag previous_balance-payments_since is set:
+    This method works backwards to rebuild the invoice as a snapshot in time.
+    The invoice displayed will have the balances owed, and payments made,
+    reflecting the state of the account at the time of invoice generation.
+
+=cut
+
+sub _items_previous {
+
+  my $self = shift;
+
+  # simple memoize
+  if ($self->get('_items_previous')) {
+    return sort { $a->{_date} <=> $b->{_date} }
+         values %{ $self->get('_items_previous') };
+  }
+
+  # Gets the customer's current balance and outstanding invoices.
+  my ($prev_balance, @open_invoices) = $self->previous;
+
+  my %invoices = map {
+    $_->invnum => $self->__items_previous_map_invoice($_)
+  } @open_invoices;
+
+  # Which credits and payments displayed on the bill will vary based on
+  # conf flag previous_balance-payments_since.
+  my @credits  = $self->_items_credits();
+  my @payments = $self->_items_payments();
+
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    # For each credit or payment, determine which invoices it was applied to.
+    # Manipulate data displayed so the invoice displayed appears as a
+    # snapshot in time... with previous balances and balance owed displayed
+    # as they were at the time of invoice creation.
+
+    my @credits_postbill = $self->_items_credits_postbill();
+    my @payments_postbill = $self->_items_payments_postbill();
+
+    my %pmnt_dupechk;
+    my %cred_dupechk;
+
+    # Each section below follows this pattern on a payment/credit
+    #
+    # - Dupe check, avoid adjusting for the same item twice
+    # - If invoice being adjusted for isn't in our list, add it
+    # - Adjust the invoice balance to refelct balnace without the
+    #   credit or payment applied
+    #
+
+    # Working with payments displayed on this bill
+    for my $pmt_hash (@payments) {
+      my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
+      for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
+        next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
+        $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
+
+        my $invnum = $cust_bill_pay->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
+      }
+    }
+
+    # Working with credits displayed on this bill
+    for my $cred_hash (@credits) {
+      my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
+      for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
+        next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
+        $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
+
+        my $invnum = $cust_credit_bill->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
       }
       }
+    }
+
+    # Working with both credits and payments which are not displayed
+    # on this bill, but which have affected this bill's balances
+    for my $postbill (@payments_postbill, @credits_postbill) {
+
+      if ($postbill->{billpaynum}) {
+        next if exists $pmnt_dupechk{$postbill->{billpaynum}};
+        $pmnt_dupechk{$postbill->{billpaynum}} = 1;
+      } elsif ($postbill->{creditbillnum}) {
+        next if exists $cred_dupechk{$postbill->{creditbillnum}};
+        $cred_dupechk{$postbill->{creditbillnum}} = 1;
+      } else {
+        die "Missing creditbillnum or billpaynum";
+      }
+
+      my $invnum = $postbill->{invnum};
+
+      $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+        unless exists $invoices{$invnum};
+
+      $invoices{$invnum}->{amount} += $postbill->{amount};
+    }
+
+    # Make sure current invoice doesn't appear in previous items
+    delete $invoices{$self->invnum}
+      if exists $invoices{$self->invnum};
 
 
-      return(\@newsections, \@newlines);
   }
 
   }
 
-  return(\@sections, \@lines);
+  # Make sure amount is formatted as a dollar string
+  # (Formatting should happen on the template side, but is not?)
+  $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
+    for keys %invoices;
+
+  $self->set('_items_previous', \%invoices);
+  return sort { $a->{_date} <=> $b->{_date} } values %invoices;
 
 }
 
 
 }
 
-sub _items { # seems to be unused
+=item _items_previous_total
+
+  Return sum of amounts from all items returned by _items_previous
+  Results will vary based on invoicing conf flags
+
+=cut
+
+sub _items_previous_total {
   my $self = shift;
   my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_previous();
+  return $tot;
+}
 
 
-  #my @display = scalar(@_)
-  #              ? @_
-  #              : qw( _items_previous _items_pkg );
-  #              #: qw( _items_pkg );
-  #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
-  my @display = qw( _items_previous _items_pkg );
+sub __items_previous_get_invoice {
+  # Helper function for _items_previous
+  #
+  # Read a record from cust_bill, return a hash of it's information
+  my ($self, $invnum) = @_;
+  die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
 
 
-  my @b = ();
-  foreach my $display ( @display ) {
-    push @b, $self->$display(@_);
-  }
-  @b;
+  my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
+  return $self->__items_previous_map_invoice($cust_bill);
 }
 
 }
 
-sub _items_previous {
-  my $self = shift;
-  my $conf = $self->conf;
-  my $cust_main = $self->cust_main;
-  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, {
-      'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
-      #'pkgpart'     => 'N/A',
-      'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%.2f", $_->owed),
-    };
+sub __items_previous_map_invoice {
+  # Helper function for _items_previous
+  #
+  # Transform a cust_bill object into a simple hash reference of the type
+  # required by _items_previous
+  my ($self, $cust_bill) = @_;
+  die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
+
+  my $date = $self->conf->exists('invoice_show_prior_due_date')
+           ? 'due '.$cust_bill->due_date2str('short')
+           : $self->time2str_local('short', $cust_bill->_date);
+
+  return {
+    invnum => $cust_bill->invnum,
+    amount => $cust_bill->owed,
+    pkgnum => 'N/A',
+    _date  => $cust_bill->_date,
+    description => join(' ',
+      $self->mt('Previous Balance, Invoice #'),
+      $cust_bill->invnum,
+      "($date)"
+    ),
   }
   }
-  @b;
-
-  #{
-  #    'description'     => 'Previous Balance',
-  #    #'pkgpart'         => 'N/A',
-  #    'pkgnum'          => 'N/A',
-  #    'amount'          => sprintf("%10.2f", $pr_total ),
-  #    'ext_description' => [ map {
-  #                                 "Invoice ". $_->invnum.
-  #                                 " (". time2str("%x",$_->_date). ") ".
-  #                                 sprintf("%10.2f", $_->owed)
-  #                         } @pr_cust_bill ],
-
-  #};
 }
 
 }
 
-=item _items_pkg [ OPTIONS ]
+=item _items_credits()
 
 
-Return line item hashes for each package item on this invoice. Nearly 
-equivalent to 
+  Return array of hashrefs containing credits to be shown as line-items
+  when rendering this bill.
 
 
-$self->_items_cust_bill_pkg([ $self->cust_bill_pkg ])
+  keys for each credit item:
+  - crednum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
 
 
-The only OPTIONS accepted is 'section', which may point to a hashref 
-with a key named 'condensed', which may have a true value.  If it 
-does, this method tries to merge identical items into items with 
-'quantity' equal to the number of items (not the sum of their 
-separate quantities, for some reason).
+  This method has three ways it selects which credits to display on
+  this bill:
+
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+     Returns credits that have been applied to this bill only
+
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
+
+     List all credits that have been recorded during the time period
+     between the timestamps of the last invoice and this invoice
+
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
+
+    List all payments that have been recorded between the timestamps
+    of the previous invoice and the following invoice.
+
+     This is used to give the customer a receipt for a payment
+     in the form of their last bill with the payment amended.
+
+     I am concerned with this implementation, but leaving in place as is
+     If this option is selected, while viewing an older bill, the old bill
+     will show ALL future credits for future bills, but no charges for
+     future bills.  Somebody could be misled into believing they have a
+     large account credit when they don't.  Also, interrupts the chain of
+     invoices as an account history... the customer could have two invoices
+     in their fileing cabinet, for two different dates, both with a line item
+     for the same duplicate credit.  The accounting is technically accurate,
+     but somebody could easily become confused and think two credits were
+     made, when really those two line items on two different bills represent
+     only a single credit
 
 =cut
 
 
 =cut
 
-sub _items_pkg {
-  my $self = shift;
-  my %options = @_;
+sub _items_credits {
+
+  my $self= shift;
+
+  # Simple memoize
+  return @{$self->get('_items_credits')} if $self->get('_items_credits');
+
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+  my $trim_len = $opt{template} || $self->get('trim_len') || 40;
+
+  my @return;
+  my @cust_credit_objs;
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+      # Case 3 (see above)
+      # Return credits timestamped between the previous and following bills
 
 
-  warn "$me _items_pkg searching for all package line items\n"
-    if $DEBUG > 1;
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
 
 
-  my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
 
 
-  warn "$me _items_pkg filtering line items\n"
-    if $DEBUG > 1;
-  my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+      my %query = (
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_credit_objs = qsearch(\%query);
+
+    } else {
+      # Case 2 (see above)
+      # Return credits timestamps between this and the previous bills
 
 
-  if ($options{section} && $options{section}->{condensed}) {
+      my $date_start = 0;
+      my $date_end = $self->_date;
 
 
-    warn "$me _items_pkg condensing section\n"
-      if $DEBUG > 1;
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
 
 
-    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};
+      @cust_credit_objs = qsearch({
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
+      });
     }
     }
-    @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;
+
+  } else {
+    # Case 1 (see above)
+    # Return only credits that have been applied to this bill
+
+    @cust_credit_objs = $self->cust_credited;
+
   }
 
   }
 
-  warn "$me _items_pkg returning ". scalar(@items). " items\n"
-    if $DEBUG > 1;
+  # Translate objects into hashrefs
+  foreach my $obj ( @cust_credit_objs ) {
+    my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
+    my %r_obj = (
+      amount       => sprintf('%.2f',$cust_credit->amount),
+      crednum      => $cust_credit->crednum,
+      _date        => $cust_credit->_date,
+      creditreason => $cust_credit->reason,
+    );
+
+    my $reason = substr($cust_credit->reason, 0, $trim_len);
+    $reason .= '...' if length($reason) < length($cust_credit->reason);
+    $reason = "($reason)" if $reason;
 
 
-  @items;
-}
+    $r_obj{description} = join(' ',
+      $self->mt('Credit applied'),
+      $self->time2str_local('short', $cust_credit->_date),
+      $reason,
+    );
 
 
-sub _taxsort {
-  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;
+    push @return, \%r_obj;
+  }
+  $self->set('_items_credits',\@return);
+  @return;
 }
 
 }
 
-sub _items_tax {
+=item _items_credits_total
+
+  Return the total of al items from _items_credits
+  Will vary based on invoice display conf flag
+
+=cut
+
+sub _items_credits_total {
   my $self = shift;
   my $self = shift;
-  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_credits();
+  return $tot;
 }
 
 }
 
-=item _items_cust_bill_pkg CUST_BILL_PKGS OPTIONS
 
 
-Takes an arrayref of L<FS::cust_bill_pkg> objects, and returns a
-list of hashrefs describing the line items they generate on the invoice.
 
 
-OPTIONS may include:
+=item _items_credits_postbill()
 
 
-format: the invoice format.
+  Returns an array of hashrefs for credits where
+  - Credit issued after this invoice
+  - Credit applied to an invoice before this invoice
 
 
-escape_function: the function used to escape strings.
+  Returned hashrefs are of the format returned by _items_credits()
+
+=cut
 
 
-DEPRECATED? (expensive, mostly unused?)
-format_function: the function used to format CDRs.
+sub _items_credits_postbill {
+  my $self = shift;
 
 
-section: a hashref containing 'description'; if this is present, 
-cust_bill_pkg_display records not belonging to this section are 
-ignored.
+  my @cust_credit_bill = qsearch({
+    table   => 'cust_credit_bill',
+    select  => join(', ',qw(
+      cust_credit_bill.creditbillnum
+      cust_credit_bill._date
+      cust_credit_bill.invnum
+      cust_credit_bill.amount
+    )),
+    addl_from => ' LEFT JOIN cust_credit'.
+                 ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
+    extra_sql => ' WHERE cust_credit.custnum     = '.$self->custnum.
+                 ' AND   cust_credit_bill._date  > '.$self->_date.
+                 ' AND   cust_credit_bill.invnum < '.$self->invnum.' ',
+#! did not investigate why hashref doesn't work for this join query
+#    hashref => {
+#      'cust_credit.custnum'     => {op => '=', value => $self->custnum},
+#      'cust_credit_bill._date'  => {op => '>', value => $self->_date},
+#      'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
+#    },
+  });
 
 
-multisection: a flag indicating that this is a multisection invoice,
-which does something complicated.
+  return map {{
+    _date         => $_->_date,
+    invnum        => $_->invnum,
+    amount        => $_->amount,
+    creditbillnum => $_->creditbillnum,
+  }} @cust_credit_bill;
+}
 
 
-multilocation: a flag to display the location label for the package.
+=item _items_payments_postbill()
 
 
-Returns a list of hashrefs, each of which may contain:
+  Returns an array of hashrefs for payments where
+  - Payment occured after this invoice
+  - Payment applied to an invoice before this invoice
 
 
-pkgnum, description, amount, unit_amount, quantity, _is_setup, and 
-ext_description, which is an arrayref of detail lines to show below 
-the package line.
+  Returned hashrefs are of the format returned by _items_payments()
 
 =cut
 
 
 =cut
 
-sub _items_cust_bill_pkg {
+sub _items_payments_postbill {
   my $self = shift;
   my $self = shift;
-  my $conf = $self->conf;
-  my $cust_bill_pkgs = shift;
-  my %opt = @_;
 
 
-  my $format = $opt{format} || '';
-  my $escape_function = $opt{escape_function} || sub { shift };
-  my $format_function = $opt{format_function} || '';
-  my $no_usage = $opt{no_usage} || '';
-  my $unsquelched = $opt{unsquelched} || ''; #unused
-  my $section = $opt{section}->{description} if $opt{section};
-  my $summary_page = $opt{summary_page} || ''; #unused
-  my $multilocation = $opt{multilocation} || '';
-  my $multisection = $opt{multisection} || '';
-  my $discount_show_always = 0;
-
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
-
-  my $cust_main = $self->cust_main;#for per-agent cust_bill-line_item-ate_style
-
-  my @b = ();
-  my ($s, $r, $u) = ( undef, undef, undef );
-  foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
-  {
-
-    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
-          || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-          || (   $_->{_is_setup} && $_->{setup_show_zero} )
-        ;
-        $_ = undef;
-      }
-    }
+  my @cust_bill_pay = qsearch({
+    table    => 'cust_bill_pay',
+    select => join(', ',qw(
+      cust_bill_pay.billpaynum
+      cust_bill_pay._date
+      cust_bill_pay.invnum
+      cust_bill_pay.amount
+    )),
+    addl_from => ' LEFT JOIN cust_bill'.
+                 ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
+    extra_sql => ' WHERE cust_bill.custnum     = '.$self->custnum.
+                 ' AND   cust_bill_pay._date   > '.$self->_date.
+                 ' AND   cust_bill_pay.invnum  < '.$self->invnum.' ',
+  });
 
 
-    my @cust_bill_pkg_display = $cust_bill_pkg->cust_bill_pkg_display;
+  return map {{
+    _date      => $_->_date,
+    invnum     => $_->invnum,
+    amount     => $_->amount,
+    billpaynum => $_->billpaynum,
+  }} @cust_bill_pay;
+}
 
 
-    warn "$me _items_cust_bill_pkg considering cust_bill_pkg ".
-         $cust_bill_pkg->billpkgnum. ", pkgnum ". $cust_bill_pkg->pkgnum. "\n"
-      if $DEBUG > 1;
+=item _items_payments()
 
 
-    foreach my $display ( grep { defined($section)
-                                 ? $_->section eq $section
-                                 : 1
-                               }
-                          #grep { !$_->summary || !$summary_page } # bunk!
-                          grep { !$_->summary || $multisection }
-                          @cust_bill_pkg_display
-                        )
-    {
+  Return array of hashrefs containing payments to be shown as line-items
+  when rendering this bill.
 
 
-      warn "$me _items_cust_bill_pkg considering cust_bill_pkg_display ".
-           $display->billpkgdisplaynum. "\n"
-        if $DEBUG > 1;
+  keys for each payment item:
+  - paynum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
 
 
-      my $type = $display->type;
+  This method has three ways it selects which payments to display on
+  this bill:
 
 
-      my $desc = $cust_bill_pkg->desc;
-      $desc = substr($desc, 0, $maxlength). '...'
-        if $format eq 'latex' && length($desc) > $maxlength;
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
 
 
-      my %details_opt = ( 'format'          => $format,
-                          'escape_function' => $escape_function,
-                          'format_function' => $format_function,
-                          'no_usage'        => $opt{'no_usage'},
-                        );
-
-      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;
-
-        # start/end dates for invoice formats that do nonstandard 
-        # things with them
-        my %item_dates = map { $_ => $cust_bill_pkg->$_ } ('sdate', 'edate');
-
-        if (    (!$type || $type eq 'S')
-             && (    $cust_bill_pkg->setup != 0
-                  || $cust_bill_pkg->setup_show_zero
-                )
-           )
-         {
-
-          warn "$me _items_cust_bill_pkg adding setup\n"
-            if $DEBUG > 1;
-
-          my $description = $desc;
-          $description .= ' Setup'
-            if $cust_bill_pkg->recur != 0
-            || $discount_show_always
-            || $cust_bill_pkg->recur_show_zero;
-
-          my @d = ();
-          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, $maxlength). '...'
-                if $format eq 'latex' && length($loc) > $maxlength;
-              push @d, &{$escape_function}($loc);
-            }
-
-          } #unless hiding service details
-
-          push @d, $cust_bill_pkg->details(%details_opt)
-            if $cust_bill_pkg->recur == 0;
-
-          if ( $cust_bill_pkg->hidden ) {
-            $s->{amount}      += $cust_bill_pkg->setup;
-            $s->{unit_amount} += $cust_bill_pkg->unitsetup;
-            push @{ $s->{ext_description} }, @d;
-          } else {
-            $s = {
-              _is_setup       => 1,
-              description     => $description,
-              #pkgpart         => $part_pkg->pkgpart,
-              pkgnum          => $cust_bill_pkg->pkgnum,
-              amount          => $cust_bill_pkg->setup,
-              setup_show_zero => $cust_bill_pkg->setup_show_zero,
-              unit_amount     => $cust_bill_pkg->unitsetup,
-              quantity        => $cust_bill_pkg->quantity,
-              ext_description => \@d,
-            };
-          };
+     Returns payments that have been applied to this bill only
 
 
-        }
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
 
 
-        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 $description = ($is_summary && $type && $type eq 'U')
-                            ? "Usage charges" : $desc;
-
-          #pry be a bit more efficient to look some of this conf stuff up
-          # outside the loop
-          unless (
-            $conf->exists('disable_line_item_date_ranges')
-              || $cust_pkg->part_pkg->option('disable_line_item_date_ranges',1)
-          ) {
-            my $time_period;
-            my $date_style = $conf->config( 'cust_bill-line_item-date_style',
-                                            $cust_main->agentnum
-                                          );
-            if ( defined($date_style) && $date_style eq 'month_of' ) {
-              $time_period = time2str('The month of %B', $cust_bill_pkg->sdate);
-            } elsif ( defined($date_style) && $date_style eq 'X_month' ) {
-              my $desc = $conf->config( 'cust_bill-line_item-date_description',
-                                         $cust_main->agentnum
-                                      );
-              $desc .= ' ' unless $desc =~ /\s$/;
-              $time_period = $desc. time2str('%B', $cust_bill_pkg->sdate);
-            } else {
-              $time_period =      time2str($date_format, $cust_bill_pkg->sdate).
-                           " - ". time2str($date_format, $cust_bill_pkg->edate);
-            }
-            $description .= " ($time_period)";
-          }
+     List all payments that have been recorded between the timestamps
+     of the previous invoice and this invoice
 
 
-          my @d = ();
-          my @seconds = (); # for display of usage info
-
-          #at least until cust_bill_pkg has "past" ranges in addition to
-          #the "future" sdate/edate ones... see #3032
-          my @dates = ( $self->_date );
-          my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
-          push @dates, $prev->sdate if $prev;
-          push @dates, undef if !$prev;
-
-          unless ( $cust_pkg->part_pkg->hide_svc_detail
-                || $cust_bill_pkg->itemdesc
-                || $cust_bill_pkg->hidden
-                || $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, $maxlength). '...'
-                if $format eq 'latex' && length($loc) > $maxlength;
-              push @d, &{$escape_function}($loc);
-            }
-
-            # Display of seconds_since_sqlradacct:
-            # On the invoice, when processing @detail_items, look for a field
-            # named 'seconds'.  This will contain total seconds for each 
-            # service, in the same order as @ext_description.  For services 
-            # that don't support this it will show undef.
-            if ( $conf->exists('svc_acct-usage_seconds') 
-                 and ! $cust_bill_pkg->pkgpart_override ) {
-              foreach my $cust_svc ( 
-                  $cust_pkg->h_cust_svc(@dates, 'I') 
-                ) {
-
-                # eval because not having any part_export_usage exports 
-                # is a fatal error, last_bill/_date because that's how 
-                # sqlradius_hour billing does it
-                my $sec = eval {
-                  $cust_svc->seconds_since_sqlradacct($dates[1] || 0, $dates[0]);
-                };
-                push @seconds, $sec;
-              }
-            } #if svc_acct-usage_seconds
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
 
 
-          }
+     List all payments that have been recorded between the timestamps
+     of the previous invoice and the following invoice.
 
 
-          unless ( $is_summary ) {
-            warn "$me _items_cust_bill_pkg adding details\n"
-              if $DEBUG > 1;
+     I am concerned with this implementation, but leaving in place as is
+     If this option is selected, while viewing an older bill, the old bill
+     will show ALL future payments for future bills, but no charges for
+     future bills.  Somebody could be misled into believing they have a
+     large account credit when they don't.  Also, interrupts the chain of
+     invoices as an account history... the customer could have two invoices
+     in their fileing cabinet, for two different dates, both with a line item
+     for the same duplicate payment.  The accounting is technically accurate,
+     but somebody could easily become confused and think two payments were
+     made, when really those two line items on two different bills represent
+     only a single payment.
 
 
-            #instead of omitting details entirely in this case (unwanted side
-            # effects), just omit CDRs
-            $details_opt{'no_usage'} = 1
-              if $type && $type eq 'R';
+=cut
 
 
-            push @d, $cust_bill_pkg->details(%details_opt);
-          }
+sub _items_payments {
 
 
-          warn "$me _items_cust_bill_pkg calculating amount\n"
-            if $DEBUG > 1;
-  
-          my $amount = 0;
-          if (!$type) {
-            $amount = $cust_bill_pkg->recur;
-          } elsif ($type eq 'R') {
-            $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
-          } elsif ($type eq 'U') {
-            $amount = $cust_bill_pkg->usage;
-          }
-  
-          if ( !$type || $type eq 'R' ) {
-
-            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;
-              push @{ $r->{ext_description} }, @d;
-            } else {
-              $r = {
-                description     => $description,
-                #pkgpart         => $part_pkg->pkgpart,
-                pkgnum          => $cust_bill_pkg->pkgnum,
-                amount          => $amount,
-                recur_show_zero => $cust_bill_pkg->recur_show_zero,
-                unit_amount     => $cust_bill_pkg->unitrecur,
-                quantity        => $cust_bill_pkg->quantity,
-                %item_dates,
-                ext_description => \@d,
-              };
-              $r->{'seconds'} = \@seconds if grep {defined $_} @seconds;
-            }
-
-          } else {  # $type eq 'U'
-
-            warn "$me _items_cust_bill_pkg adding usage\n"
-              if $DEBUG > 1;
-
-            if ( $cust_bill_pkg->hidden ) {
-              $u->{amount}      += $amount;
-              $u->{unit_amount} += $cust_bill_pkg->unitrecur;
-              push @{ $u->{ext_description} }, @d;
-            } else {
-              $u = {
-                description     => $description,
-                #pkgpart         => $part_pkg->pkgpart,
-                pkgnum          => $cust_bill_pkg->pkgnum,
-                amount          => $amount,
-                recur_show_zero => $cust_bill_pkg->recur_show_zero,
-                unit_amount     => $cust_bill_pkg->unitrecur,
-                quantity        => $cust_bill_pkg->quantity,
-                %item_dates,
-                ext_description => \@d,
-              };
-            }
-          }
+  my $self = shift;
 
 
-        } # recurring or usage with recurring charge
+  # Simple memoize
+  return @{$self->get('_items_payments')} if $self->get('_items_payments');
 
 
-      } else { #pkgnum tax or one-shot line item (??)
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
 
 
-        warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
-          if $DEBUG > 1;
+  my @return;
+  my @cust_pay_objs;
 
 
-        if ( $cust_bill_pkg->setup != 0 ) {
-          push @b, {
-            'description' => $desc,
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-          };
-        }
-        if ( $cust_bill_pkg->recur != 0 ) {
-          push @b, {
-            'description' => "$desc (".
-                             time2str($date_format, $cust_bill_pkg->sdate). ' - '.
-                             time2str($date_format, $cust_bill_pkg->edate). ')',
-            'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-          };
-        }
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+      # Case 3 (see above)
+      # Return payments timestamped between the previous and following bills
+
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
+
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
 
 
+      my %query = (
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_pay_objs = qsearch(\%query);
+
+    } else {
+      # Case 2 (see above)
+      # Return payments timestamped between this and the previous bill
+
+      my $date_start = 0;
+      my $date_end = $self->_date;
+
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
       }
 
       }
 
+      @cust_pay_objs = qsearch({
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
+      });
     }
 
     }
 
-    $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
-                                && $conf->exists('discount-show-always'));
+  } else {
+    # Case 1 (see above)
+    # Return payments applied only to this bill
 
 
-  }
+    @cust_pay_objs = $self->cust_bill_pay;
 
 
-  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
-        || ( ! $_->{_is_setup} && $_->{recur_show_zero} )
-        || (   $_->{_is_setup} && $_->{setup_show_zero} )
-    }
   }
 
   }
 
-  warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
-    if $DEBUG > 1;
+  $self->set(
+    '_items_payments',
+    [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+  );
+  return @{ $self->get('_items_payments') };
+}
+
+=item _items_payments_total
 
 
-  @b;
+  Return a total of all records returned by _items_payments
+  Results vary based on invoicing conf flags
 
 
+=cut
+
+sub _items_payments_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_payments();
+  return $tot;
 }
 
 }
 
-sub _items_credits {
-  my( $self, %opt ) = @_;
-  my $trim_len = $opt{'trim_len'} || 60;
+sub __items_payments_make_hashref {
+  # Transform a FS::cust_pay object into a simple hashref for invoice
+  my ($self, @cust_pay_objs) = @_;
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+  my @return;
 
 
-  my @b;
-  #credits
-  foreach ( $self->cust_credited ) {
+  for my $obj (@cust_pay_objs) {
 
 
-    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+    # In case we're passed FS::cust_bill_pay (or something else?)
+    # Below, we use $obj to render amount rather than $cust_apy.
+    #   If we were passed cust_bill_pay objs, then:
+    #   $obj->amount represents the amount applied to THIS invoice
+    #   $cust_pay->amount represents the total payment, which may have
+    #       been applied accross several invoices.
+    # If we were passed cust_bill_pay objects, then the conf flag
+    # previous_balance-payments_since is NOT set, so we should not
+    # present any payments not applied to this invoice.
+    my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
 
 
-    my $reason = substr($_->cust_credit->reason, 0, $trim_len);
-    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
-    $reason = " ($reason) " if $reason;
+    my %r_obj = (
+      _date   => $cust_pay->_date,
+      amount  => sprintf("%.2f", $obj->amount),
+      paynum  => $cust_pay->paynum,
+      payinfo => $cust_pay->payby_payinfo_pretty(),
+      description => join(' ',
+        $self->mt('Payment received'),
+        $self->time2str_local('short', $cust_pay->_date),
+      ),
+    );
 
 
-    push @b, {
-      #'description' => 'Credit ref\#'. $_->crednum.
-      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
-      #                 $reason,
-      'description' => $self->mt('Credit applied').' '.
-                       time2str($date_format,$_->cust_credit->_date). $reason,
-      'amount'      => sprintf("%.2f",$_->amount),
-    };
+    if ($c_invoice_payment_details) {
+      $r_obj{description} = join(' ',
+        $r_obj{description},
+        $self->mt('via'),
+        $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
+      );
+    }
+
+    push @return, \%r_obj;
   }
   }
+  return @return;
+}
 
 
-  @b;
+=item _items_total()
 
 
-}
+  Generate the line-items to be shown on the bill in the "Totals" section
 
 
-sub _items_payments {
+  Returns a list of hashrefs, each with the keys:
+  - total_item: description field
+  - total_amount: dollar-formatted number amount
+
+  Information presented by this method varies based on Conf
+
+  Conf previous_balance-payments_due
+  - default, flag not set
+      Only transactions that were applied to this bill bill be
+      displayed and calculated intothe total.  If items exist in
+      the past-due section, those items will disappear from this
+      invoice if they have been paid off.
+
+  - previous_balance-payments_due flag is set
+      Transactions occuring after the timestsamp of this
+      invoice are not reflected on invoice line items
+
+      Only payments/credits applied between the previous invoice
+      and this one are displayed and calculated into the total
+
+  - previous_balance-payments_due && $opt{template} eq 'statement'
+      Same as above, except payments/credits occuring before the date
+      of the following invoice are also displayed and calculated into
+      the total
+
+  Conf previous_balance-exclude_from_total
+  - default, flag not set
+      The "Totals" section contains a single line item.
+      The dollar amount of this line items is a sum of old and new charges
+  - previous_balance-exclude_from_total flag is set
+      The "Totals" section contains two line items.
+      One for previous balance, one for new charges
+  !NOTE: Avent virtualization flag 'disable_previous_balance' can
+      override the global conf flag previous_balance-exclude_from_total
+
+  Conf invoice_show_prior_due_date
+  - default, flag not set
+    Total line item in the "Totals" section does not mention due date
+  - invoice_show_prior_due_date flag is set
+    Total line item in the "Totals" section includes either the due
+    date of the invoice, or the specified invoice terms
+    ? Not sure why this is called "Prior" due date, since we seem to be
+      displaying THIS due date...
+=cut
+
+sub _items_total {
   my $self = shift;
   my $self = shift;
+  my $conf = $self->conf;
 
 
-  my @b;
-  #get & print payments
-  foreach ( $self->cust_bill_pay ) {
+  my $c_multi_line_total = 0;
+  $c_multi_line_total    = 1
+    if $conf->exists('previous_balance-exclude_from_total')
+    && $self->enable_previous();
 
 
-    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+  my @line_items;
+  my $invoice_charges  = $self->charged();
 
 
-    push @b, {
-      'description' => $self->mt('Payment received').' '.
-                       time2str($date_format,$_->cust_pay->_date ),
-      'amount'      => sprintf("%.2f", $_->amount )
+  # _items_previous() is aware of conf flags
+  my $previous_balance = 0;
+  $previous_balance += $_->{amount} for $self->_items_previous();
+
+  my $total_charges;
+  my $total_descr;
+
+  if ( $previous_balance && $c_multi_line_total ) {
+    # previous balance, new charges on separate lines
+
+    push @line_items, {
+      total_amount => sprintf('%.2f',$previous_balance),
+      total_item   => $self->mt(
+        $conf->config('previous_balance-text') || 'Previous Balance'
+      ),
     };
     };
+
+    $total_charges = $invoice_charges;
+    $total_descr   = $self->mt(
+      $conf->config('previous_balance-text-total_new_charges')
+      || 'Total New Charges'
+    );
+
+  } else {
+    # previous balance and new charges combined into a single total line
+    $total_charges = $invoice_charges + $previous_balance;
+    $total_descr = $self->mt('Total Charges');
+  }
+
+  if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
+    # then the due date should be shown with Total New Charges,
+    # and should NOT be shown with the Balance Due message.
+
+    if ( $self->due_date ) {
+      $total_descr .= $self->invoice_pay_by_msg;
+    } elsif ( $self->terms ) {
+      $total_descr = join(' ',
+        $total_descr,
+        '-',
+        $self->mt($self->terms)
+      );
+    }
   }
 
   }
 
-  @b;
+  push @line_items, {
+    total_amount => sprintf('%.2f', $total_charges),
+    total_item   => $total_descr,
+  };
 
 
+  return @line_items;
 }
 
 }
 
-=item _items_discounts_avail
+=item _items_aging_balances
 
 
-Returns an array of line item hashrefs representing available term discounts
-for this invoice.  This makes the same assumptions that apply to term 
-discounts in general: that the package is billed monthly, at a flat rate, 
-with no usage charges.  A prorated first month will be handled, as will 
-a setup fee if the discount is allowed to apply to setup fees.
+  Returns an array of aged balance amounts from a given epoch timestamp.
 
 
-=cut
+  The time of day is ignored for this calculation, so that slight differences
+  on the generation time of an invoice doesn't determine which column an
+  aged balance falls into.
 
 
-sub _items_discounts_avail {
-  my $self = shift;
-  my $list_pkgnums = 0; # if any packages are not eligible for all discounts
+  Will not include any balances dated after the given timestamp in
+  the calculated totals
+
+  usage:
+  @aged_balances = $b->_items_aging_balances( $b->_date )
 
 
-  my %plans = $self->discount_plans;
+  @aged_balances = (
+    under30d,
+    30d-60d,
+    60d-90d,
+    over90d
+  )
+
+=cut
 
 
-  $list_pkgnums = grep { $_->list_pkgnums } values %plans;
+sub _items_aging_balances {
+  my ($self, $basetime) = @_;
+  die "Incorrect usage of _items_aging_balances()" unless ref $self;
 
 
-  map {
-    my $months = $_;
-    my $plan = $plans{$months};
+  $basetime = $self->_date unless $basetime;
+  my @aging_balances = (0, 0, 0, 0);
+  my @open_invoices = $self->_items_previous();
+  my $d30 = 2592000; # 60 * 60 * 24 * 30,
+  my $d60 = 5184000; # 60 * 60 * 24 * 60,
+  my $d90 = 7776000; # 60 * 60 * 24 * 90
 
 
-    my $term_total = sprintf('%.2f', $plan->discounted_total);
-    my $percent = sprintf('%.0f', 
-                          100 * (1 - $term_total / $plan->base_total) );
-    my $permonth = sprintf('%.2f', $term_total / $months);
-    my $detail = $self->mt('discount on item'). ' '.
-                 join(', ', map { "#$_" } $plan->pkgnums)
-      if $list_pkgnums;
+  # Move the clock back on our given day to 12:00:01 AM
+  my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
+  my $dt_12am = DateTime->new(
+    year   => $dt_basetime->year,
+    month  => $dt_basetime->month,
+    day    => $dt_basetime->day,
+    hour   => 0,
+    minute => 0,
+    second => 1,
+  )->epoch();
 
 
-    # discounts for non-integer months don't work anyway
-    $months = sprintf("%d", $months);
+  # set our epoch breakpoints
+  $_ = $dt_12am - $_ for $d30, $d60, $d90;
 
 
-    +{
-      description => $self->mt('Save [_1]% by paying for [_2] months',
-                                $percent, $months),
-      amount      => $self->mt('[_1] ([_2] per month)', 
-                                $term_total, $money_char.$permonth),
-      ext_description => ($detail || ''),
+  # grep the aged balances
+  for my $oinv (@open_invoices) {
+    if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
+      # If post invoice dated less than 30days ago
+      $aging_balances[0] += $oinv->{amount};
+    } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
+      # If past invoice dated between 30-60 days ago
+      $aging_balances[1] += $oinv->{amount};
+    } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
+      # If past invoice dated between 60-90 days ago
+      $aging_balances[2] += $oinv->{amount};
+    } else {
+      # If past invoice dated 90+ days ago
+      $aging_balances[3] += $oinv->{amount};
     }
     }
-  } #map
-  sort { $b <=> $a } keys %plans;
+  }
+
+  return map{ sprintf('%.2f',$_) } @aging_balances;
+}
 
 
+=item has_call_details
+
+Returns true if this invoice has call details.
+
+=cut
+
+sub has_call_details {
+  my $self = shift;
+  $self->scalar_sql("
+    SELECT 1 FROM cust_bill_pkg_detail
+             LEFT JOIN cust_bill_pkg USING (billpkgnum)
+      WHERE cust_bill_pkg_detail.format = 'C'
+        AND cust_bill_pkg.invnum = ?
+      LIMIT 1
+  ", $self->invnum);
 }
 
 =item call_details [ OPTION => VALUE ... ]
 }
 
 =item call_details [ OPTION => VALUE ... ]
@@ -5425,7 +3589,7 @@ sub call_details {
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
-      
+
     };
   }
 
     };
   }
 
@@ -5439,6 +3603,18 @@ sub call_details {
   ( $header, grep { $_ ne $header } @details );
 }
 
   ( $header, grep { $_ ne $header } @details );
 }
 
+=item cust_pay_batch
+
+Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
+will be removed.
+
+=cut
+
+sub cust_pay_batch {
+  carp "FS::cust_bill->cust_pay_batch is deprecated";
+  my $self = shift;
+  qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
+}
 
 =back
 
 
 =back
 
@@ -5486,14 +3662,12 @@ sub process_respool {
   process_re_X('spool', @_);
 }
 
   process_re_X('spool', @_);
 }
 
-use Storable qw(thaw);
 use Data::Dumper;
 use Data::Dumper;
-use MIME::Base64;
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
   warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
   warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   re_X(
   warn Dumper($param) if $DEBUG;
 
   re_X(
@@ -5504,6 +3678,9 @@ sub process_re_X {
 
 }
 
 
 }
 
+# this is called from search/cust_bill.html and given all its search
+# parameters, so it needs to perform the same search.
+
 sub re_X {
   # spool_invoice ftp_invoice fax_invoice print_invoice
   my($method, $job, %param ) = @_;
 sub re_X {
   # spool_invoice ftp_invoice fax_invoice print_invoice
   my($method, $job, %param ) = @_;
@@ -5513,22 +3690,15 @@ sub re_X {
   }
 
   #some false laziness w/search/cust_bill.html
   }
 
   #some false laziness w/search/cust_bill.html
-  my $distinct = '';
-  my $orderby = 'ORDER BY cust_bill._date';
-
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
-
-  my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
-     
-  my @cust_bill = qsearch( {
-    #'select'    => "cust_bill.*",
-    'table'     => 'cust_bill',
-    'addl_from' => $addl_from,
-    'hashref'   => {},
-    'extra_sql' => $extra_sql,
-    'order_by'  => $orderby,
-    'debug' => 1,
-  } );
+  $param{'order_by'} = 'cust_bill._date';
+
+  my $query = FS::cust_bill->search(\%param);
+  delete $query->{'count_query'};
+  delete $query->{'count_addl'};
+
+  $query->{debug} = 1; # was in here before, is obviously useful
+
+  my @cust_bill = qsearch( $query );
 
   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
 
 
   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
 
@@ -5554,6 +3724,14 @@ sub re_X {
 
 }
 
 
 }
 
+sub API_getinfo {
+  my $self = shift;
+  +{ ( map { $_=>$self->$_ } $self->fields ),
+     'owed' => $self->owed,
+     #XXX last payment applied date
+   };
+}
+
 =back
 
 =head1 CLASS METHODS
 =back
 
 =head1 CLASS METHODS
@@ -5568,8 +3746,8 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
-  'charged - '. 
-    $class->paid_sql($start, $end). ' - '. 
+  'charged - '.
+    $class->paid_sql($start, $end). ' - '.
     $class->credited_sql($start, $end);
 }
 
     $class->credited_sql($start, $end);
 }
 
@@ -5624,6 +3802,13 @@ Currently only supported on PostgreSQL.
 =cut
 
 sub due_date_sql {
 =cut
 
 sub due_date_sql {
+  die "don't use: doesn't account for agent-specific invoice_default_terms";
+
+  #we're passed a $conf but not a specific customer (that's in the query), so
+  # to make this work we'd need an agentnum-aware "condition_sql_conf" like
+  # "condition_sql_option" that retreives a conf value with SQL in an agent-
+  # aware fashion
+
   my $conf = new FS::Conf;
 'COALESCE(
   SUBSTRING(
   my $conf = new FS::Conf;
 'COALESCE(
   SUBSTRING(
@@ -5636,169 +3821,6 @@ sub due_date_sql {
 ) * 86400 + cust_bill._date'
 }
 
 ) * 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
-
-=item _date
-
-List reference of start date, end date, as UNIX timestamps.
-
-=item invnum_min
-
-=item invnum_max
-
-=item agentnum
-
-=item charged
-
-List reference of charged limits (exclusive).
-
-=item owed
-
-List reference of charged limits (exclusive).
-
-=item open
-
-flag, return open invoices only
-
-=item net
-
-flag, return net invoices only
-
-=item days
-
-=item newest_percust
-
-=back
-
-Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
-
-=cut
-
-sub search_sql_where {
-  my($class, $param) = @_;
-  if ( $DEBUG ) {
-    warn "$me search_sql_where called with params: \n".
-         join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
-  }
-
-  my @search = ();
-
-  #agentnum
-  if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_main.agentnum = $1";
-  }
-
-  #agentnum
-  if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.custnum = $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";
-  }
-
-  #charged
-  if ( $param->{charged} ) {
-    my @charged = ref($param->{charged})
-                    ? @{ $param->{charged} }
-                    : ($param->{charged});
-
-    push @search, map { s/^charged/cust_bill.charged/; $_; }
-                      @charged;
-  }
-
-  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'};
-
-  #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 )';
-    #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
-
-    my @newest_where = map { my $x = $_;
-                             $x =~ s/\bcust_bill\./newest_cust_bill./g;
-                             $x;
-                           }
-                           grep ! /^cust_main./, @search;
-    my $newest_where = scalar(@newest_where)
-                         ? ' AND '. join(' AND ', @newest_where)
-                        : '';
-
-
-    push @search, "cust_bill._date = (
-      SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
-        WHERE newest_cust_bill.custnum = cust_bill.custnum
-          $newest_where
-    )";
-
-  }
-
-  #promised_date - also has an option to accept nulls
-  if ( $param->{promised_date} ) {
-    my($beginning, $ending, $null) = @{$param->{promised_date}};
-
-    push @search, "(( cust_bill.promised_date >= $beginning AND ".
-                    "cust_bill.promised_date <  $ending )" .
-                    ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
-  }
-
-  #agent virtualization
-  my $curuser = $FS::CurrentUser::CurrentUser;
-  if ( $curuser->username eq 'fs_queue'
-       && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
-    my $username = $1;
-    my $newuser = qsearchs('access_user', {
-      'username' => $username,
-      'disabled' => '',
-    } );
-    if ( $newuser ) {
-      $curuser = $newuser;
-    } else {
-      warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
-    }
-  }
-  push @search, $curuser->agentnums_sql;
-
-  join(' AND ', @search );
-
-}
-
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS
@@ -5814,4 +3836,3 @@ documentation.
 =cut
 
 1;
 =cut
 
 1;
-