RT# 78547 - Flag to disable email/print/fax/etc during tests or reports
[freeside.git] / FS / FS / cust_bill.pm
index 4fa876b..47f71c4 100644 (file)
@@ -1,47 +1,50 @@
 package FS::cust_bill;
+use base qw( FS::cust_bill::Search FS::Template_Mixin
+             FS::cust_main_Mixin FS::Record
+           );
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
-use vars qw( $invoice_lines @buf ); #yuck
+use vars qw( $DEBUG $me );
+             # but NOT $conf
+use Carp;
 use Fcntl qw(:flock); #for spool_csv
-use List::Util qw(min max);
+use Cwd;
+use List::Util qw(min max sum);
 use Date::Format;
-use Text::Template 1.20;
+use DateTime;
 use File::Temp 0.14;
-use String::ShellQuote;
 use HTML::Entities;
-use Locale::Country;
+use Storable qw( freeze thaw );
+use GD::Barcode;
 use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
+use FS::Misc qw( send_fax do_print );
 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_bill_pkg_detail;
 use FS::cust_credit;
 use FS::cust_pay;
 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_bill_pay_batch;
-use FS::part_bill_event;
 use FS::payby;
-
-@ISA = qw( FS::cust_main_Mixin FS::Record );
+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;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
 
-#ask FS::UID to run this stuff for us later
-FS::UID->install_callback( sub { 
-  $conf = new FS::Conf;
-  $money_char = $conf->config('money_char') || '$';  
-} );
-
 =head1 NAME
 
 FS::cust_bill - Object methods for cust_bill records
@@ -72,7 +75,7 @@ FS::cust_bill - Object methods for cust_bill records
   $tax_amount = $record->tax;
 
   @lines = $cust_bill->print_text;
-  @lines = $cust_bill->print_text $time;
+  @lines = $cust_bill->print_text('time' => $time);
 
 =head1 DESCRIPTION
 
@@ -81,6 +84,8 @@ owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
 following fields are currently supported:
 
+Regular fields
+
 =over 4
 
 =item invnum - primary key (assigned automatically for new invoices)
@@ -92,10 +97,40 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item charged - amount of this invoice
 
-=item printed - deprecated
+=item invoice_terms - optional terms override for this specific invoice
+
+=back
+
+Deprecated fields
+
+=over 4
+
+=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.
+
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one.  DEPRECATED.
+
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
+
+=back
+
+Specific use cases
+
+=over 4
 
 =item closed - books closed flag, empty or `Y'
 
+=item statementnum - invoice aggregation (see L<FS::cust_statement>)
+
+=item agent_invid - legacy invoice number
+
+=item promised_date - customer promised payment date, for collection
+
+=item pending - invoice is still being generated, empty or 'Y'
+
 =back
 
 =head1 METHODS
@@ -111,8 +146,16 @@ Invoices are normally created by calling the bill method of a customer object
 =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.
@@ -124,32 +167,176 @@ sub cust_unlinked_msg {
 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
 returns the error, otherwise returns false.
 
-=item delete
+=cut
+
+sub insert {
+  my $self = shift;
+  warn "$me insert called\n" if $DEBUG;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->get('cust_bill_pkg') ) {
+    foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
+      $cust_bill_pkg->invnum($self->invnum);
+      my $error = $cust_bill_pkg->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "can't create invoice line item: $error";
+      }
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
 
-This method now works but you probably shouldn't use it.  Instead, apply a
-credit against the invoice.
+}
 
-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.
+=item void [ REASON [ , REPROCESS_CDRS ] ]
 
-Really, don't use it.
+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).
 
 =cut
 
-sub delete {
+sub void {
+  my $self = shift;
+  my $reason = scalar(@_) ? shift : '';
+  my $reprocess_cdrs = scalar(@_) ? shift : '';
+
+  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 {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
-  $self->SUPER::delete(@_);
+
+  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;
+
+  foreach my $table (qw(
+    cust_credit_bill
+    cust_bill_pay_batch
+    cust_bill_pay
+    cust_bill_batch
+    cust_bill_pkg
+  )) {
+    #cust_event # problematic
+    #cust_pay_batch # unnecessary
+
+    foreach my $linked ( $self->$table() ) {
+      my $error = $linked->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
 }
 
-=item replace OLD_RECORD
+=item replace [ OLD_RECORD ]
 
-Replaces the OLD_RECORD with this one in the database.  If there is an error,
-returns the error, otherwise returns false.
+You can, but probably shouldn't modify invoices...
 
-Only printed may be changed.  printed is normally updated by calling the
-collect method of a customer object (see L<FS::cust_main>).
+Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
+supplied, replaces this record.  If there is an error, returns the error,
+otherwise returns false.
 
 =cut
 
@@ -160,15 +347,45 @@ collect method of a customer object (see L<FS::cust_main>).
 
 sub replace_check {
   my( $new, $old ) = ( shift, shift );
-  return "Can't change custnum!" unless $old->custnum == $new->custnum;
+  return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
   #return "Can't change _date!" unless $old->_date eq $new->_date;
-  return "Can't change _date!" unless $old->_date == $new->_date;
-  return "Can't change charged!" unless $old->charged == $new->charged
-                                     || $old->charged == 0;
+  return "Can't change _date" unless $old->_date == $new->_date;
+  return "Can't change charged" unless $old->charged == $new->charged
+                                    || $old->pending eq 'Y'
+                                    || $old->charged == 0
+                                   || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
   '';
 }
 
+
+=item add_cc_surcharge
+
+Giant hack
+
+=cut
+
+sub add_cc_surcharge {
+    my ($self, $pkgnum, $amount) = (shift, shift, shift);
+
+    my $error;
+    my $cust_bill_pkg = new FS::cust_bill_pkg({
+                                   'invnum' => $self->invnum,
+                                   'pkgnum' => $pkgnum,
+                                   'setup' => $amount,
+                       });
+    $error = $cust_bill_pkg->insert;
+    return $error if $error;
+
+    $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
+    $self->charged($self->charged+$amount);
+    $error = $self->replace;
+    return $error if $error;
+
+    $self->apply_payments_and_credits;
+}
+
+
 =item check
 
 Checks all fields to make sure this is a valid invoice.  If there is an error,
@@ -182,17 +399,17 @@ sub check {
 
   my $error =
     $self->ut_numbern('invnum')
-    || $self->ut_number('custnum')
+    || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
     || $self->ut_numbern('_date')
     || $self->ut_money('charged')
     || $self->ut_numbern('printed')
     || $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 "Unknown customer"
-    unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-
   $self->_date(time) unless $self->_date;
 
   $self->printed(0) if $self->printed eq '';
@@ -200,22 +417,99 @@ sub check {
   $self->SUPER::check;
 }
 
+=item display_invnum
+
+Returns the displayed invoice number for this invoice: agent_invid if
+cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
+
+=cut
+
+sub display_invnum {
+  my $self = shift;
+  if ( $self->agent_invid
+         && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
+    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
 
-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;
-  my $total = 0;
-  my @cust_bill = sort { $a->_date <=> $b->_date }
-    grep { $_->owed != 0 && $_->_date < $self->_date }
-      qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
-  ;
-  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
@@ -227,8 +521,35 @@ Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
 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 },
+      'order_by' => 'ORDER BY billpkgnum', #important?  otherwise we could use
+                                           # the AUTLOADED FK search.  or should
+                                           # that default to ORDER by the pkey?
+    }
+  );
+}
+
+=item cust_bill_pkg_pkgnum PKGNUM
+
+Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
+specified pkgnum.
+
+=cut
+
+sub cust_bill_pkg_pkgnum {
+  my( $self, $pkgnum ) = @_;
+  qsearch(
+    { 'table'    => 'cust_bill_pkg',
+      'hashref'  => { 'invnum' => $self->invnum,
+                      'pkgnum' => $pkgnum,
+                    },
       'order_by' => 'ORDER BY billpkgnum',
     }
   );
@@ -243,11 +564,24 @@ this invoice.
 
 sub cust_pkg {
   my $self = shift;
-  my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+  my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+                     $self->cust_bill_pkg;
   my %saw = ();
   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
 }
 
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+  my $self = shift;
+  grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
 =item open_cust_bill_pkg
 
 Returns the open line items for this invoice.
@@ -275,32 +609,6 @@ sub open_cust_bill_pkg {
   @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.
@@ -330,7 +638,7 @@ sub num_cust_event {
   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];
 }
@@ -339,11 +647,21 @@ sub num_cust_event {
 
 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
 
-sub cust_main {
+sub suspend {
   my $self = shift;
-  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  grep { $_->suspend(@_) }
+  grep {! $_->getfield('cancel') }
+  $self->cust_pkg;
+
 }
 
 =item cust_suspend_if_balance_over AMOUNT
@@ -365,45 +683,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
 
-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;
-}
+sub cancel {
+  my( $self, %opt ) = @_;
 
-=item cust_pay
+  warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+       join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+    if $DEBUG;
 
-Depreciated.  See the cust_bill_pay method.
+  return ( 'Access denied' )
+    unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
+  my @pkgs = $self->cust_pkg;
 
-=cut
+  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_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 } )
-  #;
+  grep { $_ }
+    map { $_->cancel(%opt) }
+      grep { ! $_->getfield('cancel') }
+        @pkgs;
 }
 
 =item cust_bill_pay
@@ -414,23 +722,136 @@ Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
 
 sub cust_bill_pay {
   my $self = shift;
+  map { $_ } #return $self->num_cust_bill_pay unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
 }
 
 =item cust_credited
 
+=item cust_credit_bill
+
 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
 
 =cut
 
 sub cust_credited {
   my $self = shift;
+  map { $_ } #return $self->num_cust_credit_bill unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
   ;
 }
 
+sub cust_credit_bill {
+  shift->cust_credited(@_);
+}
+
+#=item cust_bill_pay_pkgnum PKGNUM
+#
+#Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_bill_pay_pkgnum {
+#  my( $self, $pkgnum ) = @_;
+#  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
+#                                'pkgnum' => $pkgnum,
+#                              }
+#           );
+#}
+
+=item cust_bill_pay_pkg PKGNUM
+
+Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
+applied against the matching pkgnum.
+
+=cut
+
+sub cust_bill_pay_pkg {
+  my( $self, $pkgnum ) = @_;
+
+  qsearch({
+    'select'    => 'cust_bill_pay_pkg.*',
+    'table'     => 'cust_bill_pay_pkg',
+    'addl_from' => ' LEFT JOIN cust_bill_pay USING ( billpaynum ) '.
+                   ' LEFT JOIN cust_bill_pkg USING ( billpkgnum ) ',
+    'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+                   "   AND cust_bill_pkg.pkgnum = $pkgnum",
+  });
+
+}
+
+#=item cust_credited_pkgnum PKGNUM
+#
+#=item cust_credit_bill_pkgnum PKGNUM
+#
+#Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
+#with matching pkgnum.
+#
+#=cut
+#
+#sub cust_credited_pkgnum {
+#  my( $self, $pkgnum ) = @_;
+#  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
+#  sort { $a->_date <=> $b->_date }
+#    qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
+#                                   'pkgnum' => $pkgnum,
+#                                 }
+#           );
+#}
+#
+#sub cust_credit_bill_pkgnum {
+#  shift->cust_credited_pkgnum(@_);
+#}
+
+=item cust_credit_bill_pkg PKGNUM
+
+Returns all credit applications (see L<FS::cust_credit_bill>) for this invoice
+applied against the matching pkgnum.
+
+=cut
+
+sub cust_credit_bill_pkg {
+  my( $self, $pkgnum ) = @_;
+
+  qsearch({
+    'select'    => 'cust_credit_bill_pkg.*',
+    'table'     => 'cust_credit_bill_pkg',
+    'addl_from' => ' LEFT JOIN cust_credit_bill USING ( creditbillnum ) '.
+                   ' LEFT JOIN cust_bill_pkg    USING ( billpkgnum    ) ',
+    'extra_sql' => ' WHERE cust_bill_pkg.invnum = '. $self->invnum.
+                   "   AND cust_bill_pkg.pkgnum = $pkgnum",
+  });
+
+}
+
+=item cust_bill_batch
+
+Returns all invoice batch records (L<FS::cust_bill_batch>) for this invoice.
+
+=cut
+
+sub cust_bill_batch {
+  my $self = shift;
+  qsearch('cust_bill_batch', { 'invnum' => $self->invnum });
+}
+
+=item discount_plans
+
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
+hash keyed by term length.
+
+=cut
+
+sub discount_plans {
+  my $self = shift;
+  FS::discount_plan->all($self);
+}
+
 =item tax
 
 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
@@ -464,15 +885,86 @@ sub owed {
   $balance;
 }
 
-=item apply_payments_and_credits
+=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 apply_payments_and_credits {
+sub owed_on_invoice {
   my $self = shift;
 
-  local $SIG{HUP} = 'IGNORE';
-  local $SIG{INT} = 'IGNORE';
+  #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 ) = @_;
+
+  #my $balance = $self->charged;
+  my $balance = 0;
+  $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
+
+  $balance -= $_->amount            for $self->cust_bill_pay_pkg($pkgnum);
+  $balance -= $_->amount            for $self->cust_credit_bill_pkg($pkgnum);
+
+  $balance = sprintf( "%.2f", $balance);
+  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+  $balance;
+}
+
+=item hide
+
+Returns true if this invoice should be hidden.  See the
+selfservice-hide_invoices-taxclass configuraiton setting.
+
+=cut
+
+sub hide {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $hide_taxclass = $conf->config('selfservice-hide_invoices-taxclass')
+    or return '';
+  my @cust_bill_pkg = $self->cust_bill_pkg;
+  my @part_pkg = grep $_, map $_->part_pkg, @cust_bill_pkg;
+  ! grep { $_->taxclass ne $hide_taxclass } @part_pkg;
+}
+
+=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
+'payment_receipt_email' configuration option is set.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub apply_payments_and_credits {
+  my( $self, %options ) = @_;
+  my $conf = $self->conf;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
   local $SIG{TERM} = 'IGNORE';
   local $SIG{TSTP} = 'IGNORE';
@@ -484,9 +976,18 @@ sub apply_payments_and_credits {
 
   $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') ) {
+    # limit @payments & @credits to those w/ a pkgnum grepped from $self
+    my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
+    @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
+    @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
+  }
+
   while ( $self->owed > 0 and ( @payments || @credits ) ) {
 
     my $app = '';
@@ -504,7 +1005,7 @@ sub apply_payments_and_credits {
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
-            grep { $_ } 
+            grep { $_ }
              map  { $_->cust_pkg }
                   @open_lineitems
            );
@@ -515,7 +1016,7 @@ sub apply_payments_and_credits {
       } else {
         $app = 'credit';
       }
-    
+
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
@@ -524,31 +1025,42 @@ sub apply_payments_and_credits {
       die "guru meditation #12 and 35";
     }
 
+    my $unapp_amount;
     if ( $app eq 'pay' ) {
 
       my $payment = shift @payments;
-
-      $app = new FS::cust_bill_pay {
-        'paynum'  => $payment->paynum,
-       'amount'  => sprintf('%.2f', min( $payment->unapplied, $self->owed ) ),
-      };
+      $unapp_amount = $payment->unapplied;
+      $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
+      $app->pkgnum( $payment->pkgnum )
+        if $conf->exists('pkg-balances') && $payment->pkgnum;
 
     } elsif ( $app eq 'credit' ) {
 
       my $credit = shift @credits;
-
-      $app = new FS::cust_credit_bill {
-        'crednum' => $credit->crednum,
-       'amount'  => sprintf('%.2f', min( $credit->credited, $self->owed ) ),
-      };
+      $unapp_amount = $credit->credited;
+      $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
+      $app->pkgnum( $credit->pkgnum )
+        if $conf->exists('pkg-balances') && $credit->pkgnum;
 
     } else {
       die "guru meditation #12 and 35";
     }
 
+    my $owed;
+    if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
+      warn "owed_pkgnum ". $app->pkgnum;
+      $owed = $self->owed_pkgnum($app->pkgnum);
+    } else {
+      $owed = $self->owed;
+    }
+    next unless $owed > 0;
+
+    warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
+    $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
+
     $app->invnum( $self->invnum );
 
-    my $error = $app->insert;
+    my $error = $app->insert(%options);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return "Error inserting ". $app->table. " record: $error";
@@ -562,404 +1074,213 @@ sub apply_payments_and_credits {
 
 }
 
-=item generate_email OPTION => VALUE ...
-
-Options:
-
-=over 4
-
-=item from
+=item send HASHREF
 
-sender address, required
-
-=item tempate
+Sends this invoice to the destinations configured for this customer: sends
+email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
-alternate template name, optional
+Options can be passed as a hashref.  Positional parameters are no longer
+allowed.
 
-=item print_text
+I<template>: a suffix for alternate invoices
 
-text attachment arrayref, optional
+I<agentnum>: obsolete, now does nothing.
 
-=item subject
+I<from> overrides the default email invoice From: address.
 
-email subject, 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
 
-use MIME::Entity;
-
-sub generate_email {
-
+sub send {
   my $self = shift;
-  my %args = @_;
-
-  my $me = '[FS::cust_bill::generate_email]';
-
-  my %return = (
-    'from'      => $args{'from'},
-    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
-  );
-
-  if (ref($args{'to'}) eq 'ARRAY') {
-    $return{'to'} = $args{'to'};
-  } else {
-    $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
-                           $self->cust_main->invoicing_list
-                    ];
-  }
-
-  if ( $conf->exists('invoice_html') ) {
-
-    warn "$me creating HTML/text multipart message"
-      if $DEBUG;
-
-    $return{'nobody'} = 1;
-
-    my $alternative = build MIME::Entity
-      'Type'        => 'multipart/alternative',
-      'Encoding'    => '7bit',
-      'Disposition' => 'inline'
-    ;
+  my $opt = ref($_[0]) ? $_[0] : +{ @_ };
+  my $conf = $self->conf;
 
-    my $data;
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
-
-      warn "$me using 'invoice_email_pdf_note' in multipart message"
-        if $DEBUG;
-      $data = [ map { $_ . "\n" }
-                    $conf->config('invoice_email_pdf_note')
-              ];
-
-    } 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('', $args{'template'}) ];
-      }
-
-    }
-
-    $alternative->attach(
-      'Type'        => 'text/plain',
-      #'Encoding'    => 'quoted-printable',
-      'Encoding'    => '7bit',
-      'Data'        => $data,
-      'Disposition' => 'inline',
-    );
-
-    $args{'from'} =~ /\@([\w\.\-]+)/;
-    my $from = $1 || 'example.com';
-    my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-
-    my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
-    my $file;
-    if ( defined($args{'template'}) && length($args{'template'})
-         && -e "$path/logo_". $args{'template'}. ".png"
-       )
-    {
-      $file = "$path/logo_". $args{'template'}. ".png";
-    } else {
-      $file = "$path/logo.png";
-    }
-
-    my $image = build MIME::Entity
-      'Type'       => 'image/png',
-      'Encoding'   => 'base64',
-      'Path'       => $file,
-      'Filename'   => 'logo.png',
-      'Content-ID' => "<$content_id>",
-    ;
-
-    $alternative->attach(
-      'Type'        => 'text/html',
-      'Encoding'    => 'quoted-printable',
-      'Data'        => [ '<html>',
-                         '  <head>',
-                         '    <title>',
-                         '      '. encode_entities($return{'subject'}), 
-                         '    </title>',
-                         '  </head>',
-                         '  <body bgcolor="#e8e8e8">',
-                         $self->print_html('', $args{'template'}, $content_id),
-                         '  </body>',
-                         '</html>',
-                       ],
-      'Disposition' => 'inline',
-      #'Filename'    => 'invoice.pdf',
-    );
-
-    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'
-      );
+  my $cust_main = $self->cust_main;
 
-      $related->add_part($alternative);
+  my @invoicing_list = $cust_main->invoicing_list;
 
-      $related->add_part($image);
+  $self->email($opt)
+    if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
+    && ! $cust_main->invoice_noemail;
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+  $self->print($opt)
+    if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-      $return{'mimeparts'} = [ $related, $pdf ];
+  #this has never been used post-$ORIGINAL_ISP afaik
+  $self->fax_invoice($opt)
+    if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
-    } else {
+  '';
 
-      #no other attachment:
-      # multipart/related
-      #   multipart/alternative
-      #     text/plain
-      #     text/html
-      #   image/png
+}
 
-      $return{'content-type'} = 'multipart/related';
-      $return{'mimeparts'} = [ $alternative, $image ];
-      $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
-      #$return{'disposition'} = 'inline';
+sub email {
+  my $self = shift;
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die ref($self). '->email called with positional parameters';
+  }
 
-    }
-  
-  } else {
+  my $conf = $self->conf;
 
-    if ( $conf->exists('invoice_email_pdf') ) {
-      warn "$me creating PDF attachment"
-        if $DEBUG;
+  my $from = delete $opt->{from};
 
-      #mime parts arguments a la MIME::Entity->build().
-      $return{'mimeparts'} = [
-        { $self->mimebuild_pdf('', $args{'template'}) }
-      ];
-    }
-  
-    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 );
 
-      warn "$me using 'invoice_email_pdf_note'"
-        if $DEBUG;
-      $return{'body'} = [ map { $_ . "\n" }
-                              $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 {
-
-      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('', $args{'template'}) ];
-      }
-
+      #default: better to notify this person than silence
+      @invoicing_list = ($from);
     }
-
   }
 
-  %return;
-
-}
-
-=item mimebuild_pdf
-
-Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
-
-=cut
+  $self->SUPER::email( {
+    'from' => $from,
+    'to'   => \@invoicing_list,
+    %$opt,
+  });
 
-sub mimebuild_pdf {
-  my $self = shift;
-  (
-    'Type'        => 'application/pdf',
-    'Encoding'    => 'base64',
-    'Data'        => [ $self->print_pdf(@_) ],
-    'Disposition' => 'attachment',
-    'Filename'    => 'invoice.pdf',
-  );
 }
 
-=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Sends this invoice to the destinations configured for this customer: sends
-email, prints and/or faxes.  See L<FS::cust_main_invoice>.
-
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
-
-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.
-
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
-
-AMOUNT, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
-
-=cut
-
-sub queueable_send {
+#this stays here for now because its explicitly used as
+# FS::cust_bill::queueable_email
+sub queueable_email {
   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};
+  $self->set('mode', $opt{mode})
+    if $opt{mode};
 
-  my $error = $self->send( @args );
+  my %args = map {$_ => $opt{$_}}
+             grep { $opt{$_} }
+              qw( from notice_name no_coupon template );
+
+  my $error = $self->email( \%args );
   die $error if $error;
 
 }
 
-sub send {
+sub email_subject {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  if ( scalar(@_) && $_[0]  ) {
-    my $agentnums = ref($_[0]) ? shift : [ shift ];
-    return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
-  }
-
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
-
-  my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
-
-  return ''
-    unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
-
-  my @invoicing_list = $self->cust_main->invoicing_list;
+  my $conf = $self->conf;
 
-  #$self->email_invoice($template, $invoice_from)
-  $self->email($template, $invoice_from)
-    if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
+  #my $template = scalar(@_) ? shift : '';
+  #per-template?
 
-  #$self->print_invoice($template)
-  $self->print($template)
-    if grep { $_ eq 'POST' } @invoicing_list; #postal
-
-  $self->fax_invoice($template)
-    if grep { $_ eq 'FAX' } @invoicing_list; #fax
-
-  '';
-
-}
-
-=item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
-
-Emails this invoice.
-
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
-
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
-
-=cut
-
-sub queueable_email {
-  my %opt = @_;
+  my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
+                || 'Invoice';
 
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my @args = ( $opt{template} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
-
-  my $error = $self->email( @args );
-  die $error if $error;
+  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");
 }
 
-#sub email_invoice {
-sub email {
+sub pdf_filename {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
-
-  my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
-                            $self->cust_main->invoicing_list;
-
-  #better to notify this person than silence
-  @invoicing_list = ($invoice_from) unless @invoicing_list;
-
-  my $error = send_email(
-    $self->generate_email(
-      'from'       => $invoice_from,
-      'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
-      'template'   => $template,
-    )
-  );
-  die "can't email invoice: $error\n" if $error;
-  #die "$error\n" if $error;
-
+  'Invoice-'. $self->invnum. '.pdf';
 }
 
-=item lpr_data [ TEMPLATENAME ]
+=item lpr_data HASHREF
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options must be passed as a hashref.  Positional parameters are no longer
+allowed.
+
+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, $template) = @_;
-  $conf->exists('invoice_latex')
-    ? [ $self->print_ps('', $template) ]
-    : [ $self->print_text('', $template) ];
+  my $self = shift;
+  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 print [ TEMPLATENAME ]
+=item print HASHREF
 
 Prints this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options must be passed as a hashref.
+
+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;
-  my $template = scalar(@_) ? shift : '';
+  return if $self->hide;
+  my $conf = $self->conf;
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::print called with positional parameters";
+  }
 
-  do_print $self->lpr_data($template);
+  my $lpr = delete $opt->{lpr};
+  if($conf->exists('invoice_print_pdf')) {
+    # Add the invoice to the current batch.
+    $self->batch_invoice($opt);
+  }
+  else {
+    do_print(
+      $self->lpr_data($opt),
+      'agentnum' => $self->cust_main->agentnum,
+      'lpr'      => $lpr,
+    );
+  }
 }
 
-=item fax_invoice [ TEMPLATENAME ] 
+=item fax_invoice HASHREF
 
 Faxes this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options must be passed as a hashref.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 sub fax_invoice {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
+  return if $self->hide;
+  my $conf = $self->conf;
+  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.'
     unless $conf->exists('invoice_latex');
@@ -967,14 +1288,60 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my $error = send_fax( 'docdata'    => $self->lpr_data($template),
+  my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
 
 }
 
-=item ftp_invoice [ TEMPLATENAME ] 
+=item batch_invoice [ HASHREF ]
+
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there
+isn't an open batch, one will be created.
+
+HASHREF may contain any options to be passed to C<print_pdf>.
+
+=cut
+
+sub batch_invoice {
+  my ($self, $opt) = @_;
+  my $bill_batch = $self->get_open_bill_batch;
+  my $cust_bill_batch = FS::cust_bill_batch->new({
+      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);
+}
+
+=item get_open_batch
+
+Returns the currently open batch as an FS::bill_batch object, creating a new
+one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
+enabled)
+
+=cut
+
+sub get_open_bill_batch {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $hashref = { status => 'O' };
+  $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
+                             ? $self->cust_main->agentnum
+                             : '';
+  my $batch = qsearchs('bill_batch', $hashref);
+  return $batch if $batch;
+  $batch = FS::bill_batch->new($hashref);
+  my $error = $batch->insert;
+  die $error if $error;
+  return $batch;
+}
+
+=item ftp_invoice [ TEMPLATENAME ]
 
 Sends this invoice data via FTP.
 
@@ -984,6 +1351,7 @@ TEMPLATENAME is unused?
 
 sub ftp_invoice {
   my $self = shift;
+  my $conf = $self->conf;
   my $template = scalar(@_) ? shift : '';
 
   $self->send_csv(
@@ -996,27 +1364,23 @@ sub ftp_invoice {
   );
 }
 
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+=item spool_invoice [ TEMPLATENAME ]
+
+Spools this invoice data (see L<FS::spool_csv>)
 
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
+TEMPLATENAME is unused?
 
 =cut
 
-sub send_if_newest {
+sub spool_invoice {
   my $self = shift;
+  my $conf = $self->conf;
+  my $template = scalar(@_) ? 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(@_);
+  $self->spool_csv(
+    'format'       => $conf->config('cust_bill-spoolformat'),
+    'agent_spools' => $conf->exists('cust_bill-spoolagent'),
+  );
 }
 
 =item send_csv OPTION => VALUE, ...
@@ -1041,14 +1405,20 @@ See L</print_csv> for a description of the output format.
 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;
 
+  # 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 ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
@@ -1090,13 +1460,24 @@ Options are:
 
 =over 4
 
-=item format - 'default' or 'billco'
+=item format - any of FS::Misc::::Invoicing::spool_formats
+
+=item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
+customer has the corresponding invoice destinations set (see
+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 dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
+=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 agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
+=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 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
 
@@ -1105,6 +1486,12 @@ Options are:
 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'} ) {
@@ -1122,15 +1509,27 @@ sub spool_csv {
   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 =
-    "$spooldir/".
-    ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
-    ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
-    '.csv';
-  
-  my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
+  my $file;
+  if ( $opt{'agent_spools'} ) {
+    $file = 'agentnum'.$cust_main->agentnum;
+  } else {
+    $file = 'spool';
+  }
+
+  if ( $opt{'upload_targetnum'} ) {
+    $spooldir .= '/target'.$opt{'upload_targetnum'};
+    mkdir $spooldir, 0700 unless -d $spooldir;
+  } # otherwise it just goes into export.xxx/cust_bill
+
+  if ( lc($opt{'format'}) eq 'billco' ) {
+    $file .= '-header';
+  }
+
+  $file = "$spooldir/$file.csv";
+
+  my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
   flock(CSV, LOCK_EX);
@@ -1143,17 +1542,14 @@ sub spool_csv {
     flock(CSV, LOCK_UN);
     close CSV;
 
-    $file =
-      "$spooldir/".
-      ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
-      '-detail.csv';
+    $file =~ s/-header.csv$/-detail.csv/;
 
     open(CSV,">>$file") or die "can't open $file: $!";
     flock(CSV, LOCK_EX);
     seek(CSV, 0, 2);
   }
 
-  print CSV $detail;
+  print CSV $detail if defined($detail);
 
   flock(CSV, LOCK_UN);
   close CSV;
@@ -1168,7 +1564,7 @@ Returns CSV data for this invoice.
 
 Options are:
 
-format - 'default' or 'billco'
+format - 'default', 'billco', 'oneline', 'bridgestone'
 
 Returns a list consisting of two scalars.  The first is a single line of CSV
 header information for this invoice.  The second is one or more lines of CSV
@@ -1177,7 +1573,8 @@ detail information for this invoice.
 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, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
+address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
 
@@ -1282,37 +1679,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
 
+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,
+amount charged, amount due, previous balance, due date.
+
+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
+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
+"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});
+  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 $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'}
-      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
-      $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
@@ -1347,52 +1778,245 @@ sub print_csv {
       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
     );
 
-  } else {
-  
+  } 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 {
+                      $_->{pkgnum},
+                      $_->{description},
+                      $_->{amount}
+                    }
+                  $self->_items_pkg, #_items_nontax?  no sections or anything
+                                     # with this format
+                  $self->_items_tax;
+
     $csv->combine(
-      'cust_bill',
-      $self->invnum,
+      $cust_main->agentnum,
+      $cust_main->agent->agent,
       $self->custnum,
+      $cust_main->first,
+      $cust_main->last,
+      $cust_main->company,
+      $cust_main->address1,
+      $cust_main->address2,
+      $cust_main->city,
+      $cust_main->state,
+      $cust_main->zip,
+
+      # invoice fields
       time2str("%x", $self->_date),
-      sprintf("%.2f", $self->charged),
-      ( map { $cust_main->getfield($_) }
-          qw( first last company address1 address2 city state zip country ) ),
-      map { '' } (1..5),
-    ) or die "can't create csv";
-  }
-
-  my $header = $csv->string. "\n";
-
-  my $detail = '';
-  if ( lc($opt{'format'}) eq 'billco' ) {
+      $self->invnum,
+      $self->charged,
+      $totaldue,
+      $previous_balance,
+      $self->due_date2str("%x"),
 
-    my $lineseq = 0;
-    foreach my $item ( $self->_items_pkg ) {
+      @items,
+    );
 
-      $csv->combine(
-        '',                     #  1 | N/A-Leave Empty            CHAR   2
-        '',                     #  2 | N/A-Leave Empty            CHAR  15
-        $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
-        $self->invnum,          #  4 | Invoice Number             CHAR  15
-        $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
-        $item->{'description'}, #  6 | Transaction Detail         CHAR 100
-        $item->{'amount'},      #  7 | Amount                     NUM*   9
-        '',                     #  8 | Line Format Control**      CHAR   2
-        '',                     #  9 | Grouping Code              CHAR   2
-        '',                     # 10 | User Defined               CHAR  15
+  } elsif ( $format eq 'bridgestone' ) {
+
+    # bypass the CSV stuff and just return this
+    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)
+      || '';
+    return (
+      sprintf(
+        "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
+        $prefix,
+        $cust_main->display_custnum,
+        $longdate,
+        uc(substr($cust_main->contact_firstlast,0,30)),
+        uc(substr($cust_main->company          ,0,30)),
+        uc(substr($cust_main->address1         ,0,30)),
+        uc(substr($cust_main->address2         ,0,30)),
+        uc(substr($cust_main->city             ,0,20)),
+        uc($cust_main->state),
+        $zip
+      ),
+      '' #detail
       );
 
-      $detail .= $csv->string. "\n";
+  } 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;
     }
 
-  } else {
+    # 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);
 
-    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    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,
+      $self->custnum,
+      time2str("%x", $self->_date),
+      sprintf("%.2f", $self->charged),
+      ( map { $cust_main->getfield($_) }
+          qw( first last company address1 address2 city state zip country ) ),
+      map { '' } (1..5),
+    ) or die "can't create csv";
+  }
+
+  my $header = $csv->string. "\n";
+
+  my $detail = '';
+  if ( lc($opt{'format'}) eq 'billco' ) {
+
+    my $lineseq = 0;
+    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
+        $tracctnum,             #  3 | Account Number             CHAR  15
+        $self->invnum,          #  4 | Invoice Number             CHAR  15
+        $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
+        $description,           #  6 | Transaction Detail         CHAR 100
+        $item->{'amount'},      #  7 | Amount                     NUM*   9
+        '',                     #  8 | Line Format Control**      CHAR   2
+        '',                     #  9 | Grouping Code              CHAR   2
+        '',                     # 10 | User Defined               CHAR  15
+      );
+
+      $detail .= $csv->string. "\n";
+
+    }
+
+  } elsif ( lc($opt{'format'}) eq 'oneline' ) {
+
+    #do nothing
+
+  } else {
+
+    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
-      
+
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
@@ -1401,23 +2025,21 @@ sub print_csv {
           ( $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)
             : '' ),
-          ($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;
-        my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
-                         ? ( $cust_bill_pkg->itemdesc || 'Tax' )
-                         : 'Tax';
-        ($pkg, $setup, $recur, $sdate, $edate) =
-          ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
+        $pkg = $cust_bill_pkg->desc;
+        $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
+        ( $sdate, $edate ) = ( '', '' );
       }
-  
+
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
@@ -1435,24 +2057,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 {
-  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
@@ -1498,7 +2104,9 @@ sub realtime_lec {
 }
 
 sub realtime_bop {
-  my( $self, $method ) = @_;
+  my( $self, $method ) = (shift,shift);
+  my $conf = $self->conf;
+  my %opt = @_;
 
   my $cust_main = $self->cust_main;
   my $balance = $cust_main->balance;
@@ -1524,6 +2132,15 @@ sub realtime_bop {
   $cust_main->realtime_bop($method, $amount,
     'description' => $description,
     'invnum'      => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+#    'apply'       => 1,
+    'apply_to_invoice' => 1,
+    %opt,
+ #what we want:
+ #this changes application behavior: auto payments
+                        #triggered against a specific invoice are now applied
+                        #to that invoice instead of oldest open.
+                        #seem okay to me...
   );
 
 }
@@ -1541,7 +2158,7 @@ sub batch_card {
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
-  
+
   $cust_main->batch_card(%options);
 }
 
@@ -1555,1331 +2172,1443 @@ sub _agent_invoice_from {
   $self->cust_main->agent_invoice_from;
 }
 
-=item print_text [ TIME [ , TEMPLATE ] ]
+=item invoice_barcode DIR_OR_FALSE
 
-Returns an text invoice, as a list of lines.
-
-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.
+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 print_text {
-  my( $self, $today, $template ) = @_;
+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 invnum_date_pretty
 
-  my %params = ( 'format' => 'template' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)".
+
+Intended for back-end context, with regard to translation and date formatting.
 
-  $self->print_generic( %params );
+=cut
+
+#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;
+  #$self->mt('Invoice #').
+  'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+    $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
 }
 
-=item print_latex [ TIME [ , TEMPLATE ] ]
+#sub _items_extra_usage_sections {
+#  my $self = shift;
+#  my $escape = shift;
+#
+#  my %sections = ();
+#
+#  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
+#  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
+#  {
+#    next unless $cust_bill_pkg->pkgnum > 0;
+#
+#    foreach my $section ( keys %usage_class ) {
+#
+#      my $usage = $cust_bill_pkg->usage($section);
+#
+#      next unless $usage && $usage > 0;
+#
+#      $sections{$section} ||= 0;
+#      $sections{$section} += $usage;
+#
+#    }
+#
+#  }
+#
+#  map { { 'description' => &{$escape}($_),
+#          'subtotal'    => $sections{$_},
+#          'summarized'  => '',
+#          'tax_section' => '',
+#        }
+#      }
+#    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
+#
+#}
+
+sub _items_extra_usage_sections {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $escape = shift;
+  my $format = shift;
 
-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).
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
 
-See print_ps and print_pdf for methods that return PostScript and PDF output.
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
-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.
+  my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
 
-=cut
+    foreach my $classnum ( keys %usage_class ) {
+      my $section = $usage_class{$classnum}->classname;
+      $classnums{$section} = $classnum;
 
-sub print_latex {
+      foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
+        my $amount = $detail->amount;
+        next unless $amount && $amount > 0;
 
-  my( $self, $today, $template ) = @_;
+        $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
+        $sections{$section}{amount} += $amount;  #subtotal
+        $sections{$section}{calls}++;
+        $sections{$section}{duration} += $detail->duration;
 
-  my %params = ( 'format' => 'latex' );
-  $params{'time'} = $today if $today;
-  $params{'template'} = $template if $template;
+        my $desc = $detail->regionname;
+        my $description = $desc;
+        $description = substr($desc, 0, $maxlength). '...'
+          if $format eq 'latex' && length($desc) > $maxlength;
 
-  $template ||= $self->_agent_template;
+        $lines{$section}{$desc} ||= {
+          description     => &{$escape}($description),
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_bill_pkg->pkgnum,
+          ref             => '',
+          amount          => 0,
+          calls           => 0,
+          duration        => 0,
+          #unit_amount     => $cust_bill_pkg->unitrecur,
+          quantity        => $cust_bill_pkg->quantity,
+          product_code    => 'N/A',
+          ext_description => [],
+        };
+
+        $lines{$section}{$desc}{amount} += $amount;
+        $lines{$section}{$desc}{calls}++;
+        $lines{$section}{$desc}{duration} += $detail->duration;
+
+      }
+    }
+  }
 
-  my $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 %sectionmap = ();
+  foreach (keys %sections) {
+    my $usage_class = $usage_class{$classnums{$_}};
+    $sectionmap{$_} = { 'description' => &{$escape}($_),
+                        'amount'    => $sections{$_}{amount},    #subtotal
+                        'calls'       => $sections{$_}{calls},
+                        'duration'    => $sections{$_}{duration},
+                        'summarized'  => '',
+                        'tax_section' => '',
+                        'sort_weight' => $usage_class->weight,
+                        ( $usage_class->format
+                          ? ( map { $_ => $usage_class->$_($format) }
+                              qw( description_generator header_generator total_generator total_line_generator )
+                            )
+                          : ()
+                        ),
+                      };
+  }
 
-  if ($template && $conf->exists("logo_${template}.eps")) {
-    print $lh $conf->config_binary("logo_${template}.eps")
-      or die "can't write temp file: $!\n";
-  }else{
-    print $lh $conf->config_binary('logo.eps')
-      or die "can't write temp file: $!\n";
+  my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
+                 values %sectionmap;
+
+  my @lines = ();
+  foreach my $section ( keys %lines ) {
+    foreach my $line ( keys %{$lines{$section}} ) {
+      my $l = $lines{$section}{$line};
+      $l->{section}     = $sectionmap{$section};
+      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
+      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+      push @lines, $l;
+    }
   }
-  close $lh;
-  $params{'logo_file'} = $lh->filename;
 
-  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";
-  print $fh join('', @filled_in );
-  close $fh;
+  return(\@sections, \@lines);
+
+}
+
+sub _did_summary {
+    my $self = shift;
+    my $end = $self->_date;
+
+    # start at date of previous invoice + 1 second or 0 if no previous invoice
+    my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
+    $start = 0 if !$start;
+    $start++;
+
+    my $cust_main = $self->cust_main;
+    my @pkgs = $cust_main->all_pkgs;
+    my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
+       = (0,0,0,0,0);
+    my @seen = ();
+    foreach my $pkg ( @pkgs ) {
+       my @h_cust_svc = $pkg->h_cust_svc($end);
+       foreach my $h_cust_svc ( @h_cust_svc ) {
+           next if grep {$_ eq $h_cust_svc->svcnum} @seen;
+           next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
+
+           my $inserted = $h_cust_svc->date_inserted;
+           my $deleted = $h_cust_svc->date_deleted;
+           my $phone_inserted = $h_cust_svc->h_svc_x($inserted+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
+               && (!$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
+                 local($FS::Record::qsearch_qualify_columns) = 0;
+                my $phone_portedin = qsearchs( 'h_svc_phone',
+                     { 'svcnum' => $h_cust_svc->svcnum,
+                       'lnp_status' => 'portedin' },
+                     FS::h_svc_phone->sql_h_searchs($end),
+                   );
+                $num_portedin++ if $phone_portedin;
+           }
+
+# DID either deactivated or ported out;        cannot be both for same DID simultaneously
+           if($deleted >= $start && $deleted <= $end && $phone_deleted
+               && (!$phone_deleted->lnp_status
+                   || $phone_deleted->lnp_status ne 'portingout')) {
+               $num_deactivated++;
+           }
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+               && $phone_deleted->lnp_status
+               && $phone_deleted->lnp_status eq 'portingout') {
+               $num_portedout++;
+           }
+
+           # increment usage minutes
+        if ( $phone_inserted ) {
+            my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
+            $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
+        }
+        else {
+            warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
+        }
 
-  $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
-  return ($1, $params{'logo_file'});
+           # don't look at this service again
+           push @seen, $h_cust_svc->svcnum;
+       }
+    }
 
-}
+    $minutes = sprintf("%d", $minutes);
+    ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
+       . "$num_deactivated  Ported-Out: $num_portedout ",
+           "Total Minutes: $minutes");
+}
+
+sub _items_accountcode_cdr {
+    my $self = shift;
+    my $escape = shift;
+    my $format = shift;
+
+    my $section = { 'amount'        => 0,
+                    'calls'         => 0,
+                    'duration'      => 0,
+                    'sort_weight'   => '',
+                    'phonenum'      => '',
+                    'description'   => 'Usage by Account Code',
+                    'post_total'    => '',
+                    'summarized'    => '',
+                    'header'        => '',
+                  };
+    my @lines;
+    my %accountcodes = ();
 
-=item print_generic OPTIONS_HASH
+    foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+        next unless $cust_bill_pkg->pkgnum > 0;
+
+        my @header = $cust_bill_pkg->details_header;
+        next unless scalar(@header);
+        $section->{'header'} = join(',',@header);
+
+        foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+            $section->{'header'} = $detail->formatted('format' => $format)
+                if($detail->detail eq $section->{'header'});
+
+            my $accountcode = $detail->accountcode;
+            next unless $accountcode;
+
+            my $amount = $detail->amount;
+            next unless $amount && $amount > 0;
+
+            $accountcodes{$accountcode} ||= {
+                    description => $accountcode,
+                    pkgnum      => '',
+                    ref         => '',
+                    amount      => 0,
+                    calls       => 0,
+                    duration    => 0,
+                    quantity    => '',
+                    product_code => 'N/A',
+                    section     => $section,
+                    ext_description => [ $section->{'header'} ],
+                    detail_temp => [],
+            };
+
+            $section->{'amount'} += $amount;
+            $accountcodes{$accountcode}{'amount'} += $amount;
+            $accountcodes{$accountcode}{calls}++;
+            $accountcodes{$accountcode}{duration} += $detail->duration;
+            push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
+        }
+    }
 
-Internal method - returns a filled-in template for this invoice as a scalar.
+    foreach my $l ( values %accountcodes ) {
+        $l->{amount} = sprintf( "%.2f", $l->{amount} );
+        my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
+        foreach my $sorted_detail ( @sorted_detail ) {
+            push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
+        }
+        delete $l->{detail_temp};
+        push @lines, $l;
+    }
 
-See print_ps and print_pdf for methods that return PostScript and PDF output.
+    my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
 
-Non optional options include 
-  format - latex, html, template
+    return ($section,\@sorted_lines);
+}
 
-Optional options include
+sub _items_svc_phone_sections {
+  my $self = shift;
+  my $conf = $self->conf;
+  my $escape = shift;
+  my $format = shift;
+
+  my %sections = ();
+  my %classnums = ();
+  my %lines = ();
+
+  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 };
+
+  foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
+    next unless $cust_bill_pkg->pkgnum > 0;
+
+    my @header = $cust_bill_pkg->details_header;
+    next unless scalar(@header);
+
+    foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
+
+      my $phonenum = $detail->phonenum;
+      next unless $phonenum;
+
+      my $amount = $detail->amount;
+      next unless $amount && $amount > 0;
+
+      $sections{$phonenum} ||= { 'amount'      => 0,
+                                 'calls'       => 0,
+                                 'duration'    => 0,
+                                 'sort_weight' => -1,
+                                 'phonenum'    => $phonenum,
+                                };
+      $sections{$phonenum}{amount} += $amount;  #subtotal
+      $sections{$phonenum}{calls}++;
+      $sections{$phonenum}{duration} += $detail->duration;
+
+      my $desc = $detail->regionname;
+      my $description = $desc;
+      $description = substr($desc, 0, $maxlength). '...'
+        if $format eq 'latex' && length($desc) > $maxlength;
+
+      $lines{$phonenum}{$desc} ||= {
+        description     => &{$escape}($description),
+        #pkgpart         => $part_pkg->pkgpart,
+        pkgnum          => '',
+        ref             => '',
+        amount          => 0,
+        calls           => 0,
+        duration        => 0,
+        #unit_amount     => '',
+        quantity        => '',
+        product_code    => 'N/A',
+        ext_description => [],
+      };
 
-template - a value used as a suffix for a configuration template
+      $lines{$phonenum}{$desc}{amount} += $amount;
+      $lines{$phonenum}{$desc}{calls}++;
+      $lines{$phonenum}{$desc}{duration} += $detail->duration;
+
+      my $line = $usage_class{$detail->classnum}->classname;
+      $sections{"$phonenum $line"} ||=
+        { 'amount' => 0,
+          'calls' => 0,
+          'duration' => 0,
+          'sort_weight' => $usage_class{$detail->classnum}->weight,
+          'phonenum' => $phonenum,
+          'header'  => [ @header ],
+        };
+      $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
+      $sections{"$phonenum $line"}{calls}++;
+      $sections{"$phonenum $line"}{duration} += $detail->duration;
+
+      $lines{"$phonenum $line"}{$desc} ||= {
+        description     => &{$escape}($description),
+        #pkgpart         => $part_pkg->pkgpart,
+        pkgnum          => '',
+        ref             => '',
+        amount          => 0,
+        calls           => 0,
+        duration        => 0,
+        #unit_amount     => '',
+        quantity        => '',
+        product_code    => 'N/A',
+        ext_description => [],
+      };
 
-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.
+      $lines{"$phonenum $line"}{$desc}{amount} += $amount;
+      $lines{"$phonenum $line"}{$desc}{calls}++;
+      $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
+      push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
+           $detail->formatted('format' => $format);
 
-cid - 
+    }
+  }
 
-unsquelch_cdr - overrides any per customer cdr squelching when true
+  my %sectionmap = ();
+  my $simple = new FS::usage_class { format => 'simple' }; #bleh
+  foreach ( keys %sections ) {
+    my @header = @{ $sections{$_}{header} || [] };
+    my $usage_simple =
+      new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
+    my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
+    my $usage_class = $summary ? $simple : $usage_simple;
+    my $ending = $summary ? ' usage charges' : '';
+    my %gen_opt = ();
+    unless ($summary) {
+      $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+    }
+    $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
+                        'amount'    => $sections{$_}{amount},    #subtotal
+                        'calls'       => $sections{$_}{calls},
+                        'duration'    => $sections{$_}{duration},
+                        'summarized'  => '',
+                        'tax_section' => '',
+                        'phonenum'    => $sections{$_}{phonenum},
+                        'sort_weight' => $sections{$_}{sort_weight},
+                        'post_total'  => $summary, #inspire pagebreak
+                        (
+                          ( map { $_ => $usage_class->$_($format, %gen_opt) }
+                            qw( description_generator
+                                header_generator
+                                total_generator
+                                total_line_generator
+                              )
+                          )
+                        ),
+                      };
+  }
 
-=cut
+  my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
+                        $a->{sort_weight} <=> $b->{sort_weight}
+                      }
+                 values %sectionmap;
+
+  my @lines = ();
+  foreach my $section ( keys %lines ) {
+    foreach my $line ( keys %{$lines{$section}} ) {
+      my $l = $lines{$section}{$line};
+      $l->{section}     = $sectionmap{$section};
+      $l->{amount}      = sprintf( "%.2f", $l->{amount} );
+      #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
+      push @lines, $l;
+    }
+  }
 
-sub print_generic {
+  if($conf->exists('phone_usage_class_summary')) {
+      # this only works with Latex
+      my @newlines;
+      my @newsections;
+
+      # after this, we'll have only two sections per DID:
+      # Calls Summary and Calls Detail
+      foreach my $section ( @sections ) {
+       if($section->{'post_total'}) {
+           $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
+           $section->{'total_line_generator'} = sub { '' };
+           $section->{'total_generator'} = sub { '' };
+           $section->{'header_generator'} = sub { '' };
+           $section->{'description_generator'} = '';
+           push @newsections, $section;
+           my %calls_detail = %$section;
+           $calls_detail{'post_total'} = '';
+           $calls_detail{'sort_weight'} = '';
+           $calls_detail{'description_generator'} = sub { '' };
+           $calls_detail{'header_generator'} = sub {
+               return ' & Date/Time & Called Number & Duration & Price'
+                   if $format eq 'latex';
+               '';
+           };
+           $calls_detail{'description'} = 'Calls Detail: '
+                                                   . $section->{'phonenum'};
+           push @newsections, \%calls_detail;
+       }
+      }
 
-  my( $self, %params ) = @_;
-  my $today = $params{today} ? $params{today} : time;
-  warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
-    if $DEBUG;
+      # after this, each usage class is collapsed/summarized into a single
+      # line under the Calls Summary section
+      foreach my $newsection ( @newsections ) {
+       if($newsection->{'post_total'}) { # this means Calls Summary
+           foreach my $section ( @sections ) {
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
+                               && !$section->{'post_total'});
+               my $newdesc = $section->{'description'};
+               my $tn = $section->{'phonenum'};
+               $newdesc =~ s/$tn//g;
+               my $line = {  ext_description => [],
+                             pkgnum => '',
+                             ref => '',
+                             quantity => '',
+                             calls => $section->{'calls'},
+                             section => $newsection,
+                             duration => $section->{'duration'},
+                             description => $newdesc,
+                             amount => sprintf("%.2f",$section->{'amount'}),
+                             product_code => 'N/A',
+                           };
+               push @newlines, $line;
+           }
+       }
+      }
 
-  my $format = $params{format};
-  die "Unknown format: $format"
-    unless $format =~ /^(latex|html|template)$/;
+      # 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;
+           }
+       }
+      }
 
-  my $cust_main = $self->cust_main;
-  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
-
-
-  my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
-                     'html'     => [ '<%=', '%>' ],
-                     'template' => [ '{', '}' ],
-                   );
-
-  #create the template
-  my $template = $params{template} ? $params{template} : $self->_agent_template;
-  my $templatefile = "invoice_$format";
-  $templatefile .= "_$template"
-    if length($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);
-  } 
-
-  my $text_template = new Text::Template(
-    TYPE => 'ARRAY',
-    SOURCE => \@invoice_template,
-    DELIMITERS => $delimiters{$format},
-  );
+      return(\@newsections, \@newlines);
+  }
 
-  $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 "$_", @_ },
-               },
-    '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\-]+}//;
-                       $_;
-                     }  @_
-                   },
-                 'coupon'        => 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 { "" },
-               },
-  );
+  return(\@sections, \@lines);
 
+}
 
-  # 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'     => \&encode_entities,
-                           'template' => sub { shift },
-                         );
-  my $escape_function = $escape_functions{$format};
-
-  my %date_formats = ( 'latex'    => '%b %o, %Y',
-                       'html'     => '%b&nbsp;%o,&nbsp;%Y',
-                       'template' => '%s',
-                     );
-  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};
-
-
-  # 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)
-    );
+=item _items_usage_class_summary OPTIONS
 
-  } elsif ( grep /\S/,
-            $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
+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'.
 
-    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') ) {
-
-    my $convert_map = $convert_maps{$format}{'returnaddress'};
-    $returnaddress = join( "\n", &$convert_map(
-                                   map { s/( {2,})/'~' x length($1)/eg;
-                                         s/$/\\\\\*/;
-                                         $_
-                                       }
-                                     ( $conf->config('company_name'),
-                                       $conf->config('company_address'),
-                                     )
-                                 )
-                     );
+OPTIONS can include 'escape' (a function to escape the descriptions).
 
-  } else {
+=cut
 
-    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;
+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;
+}
 
-  my %invoice_data = (
-    'company_name'    => scalar( $conf->config('company_name') ),
-    'company_address' => join("\n", $conf->config('company_address') ). "\n",
-    'custnum'         => $self->custnum,
-    'invnum'          => $self->invnum,
-    'date'            => time2str($date_format, $self->_date),
-    'today'           => time2str('%b %o, %Y', $today),
-    'agent'           => &$escape_function($cust_main->agent->agent),
-    'agent_custid'    => &$escape_function($cust_main->agent_custid),
-    'payname'         => &$escape_function($cust_main->payname),
-    'company'         => &$escape_function($cust_main->company),
-    'address1'        => &$escape_function($cust_main->address1),
-    'address2'        => &$escape_function($cust_main->address2),
-    'city'            => &$escape_function($cust_main->city),
-    'state'           => &$escape_function($cust_main->state),
-    'zip'             => &$escape_function($cust_main->zip),
-    'returnaddress'   => $returnaddress,
-    #'quantity'        => 1,
-    'terms'           => $self->terms,
-    'template'        => $params{'template'},
-    #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
-    # better hang on to conf_dir for a while
-    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
-    'page'            => 1,
-    'total_pages'     => 1,
-    'current_charges' => sprintf("%.2f", $self->charged),
-    'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
-    'ship_enable'     => $conf->exists('invoice-ship_address'),
-    'unitprices'      => $conf->exists('invoice-unitprice'),
-  );
+=item _items_previous()
 
-  my $countrydefault = $conf->config('countrydefault') || 'US';
-  my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
-  foreach ( qw( contact company address1 address2 city state zip country fax) ){
-    my $method = $prefix.$_;
-    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
-  }
-  $invoice_data{'ship_country'} = ''
-    if ( $invoice_data{'ship_country'} eq $countrydefault );
-  
-  $invoice_data{'cid'} = $params{'cid'}
-    if $params{'cid'};
-
-  if ( $cust_main->country eq $countrydefault ) {
-    $invoice_data{'country'} = '';
-  } else {
-    $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
-  }
+  Returns an array of hashrefs, each hashref representing a line-item on
+  the current bill for previous unpaid invoices.
 
-  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'};
-
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-#  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
-  #my $balance_due = $self->owed + $pr_total - $cr_total;
-  my $balance_due = $self->owed + $pr_total;
-  $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
-  $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
-
-  #do variable substitution in notes, footer, smallfooter
-  foreach my $include (qw( notes footer smallfooter coupon )) {
-
-    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
-    my @inc_src;
-
-    if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
-
-      @inc_src = $conf->config($inc_file);
+  keys for each previous_item:
+  - amount (see notes)
+  - pkgnum
+  - description
+  - invnum
+  - _date
 
-    } else {
+  Payments and credits shown on this invoice may vary based on configuraiton.
 
-      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+  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.
 
-      my $convert_map = $convert_maps{$format}{$include};
+=cut
 
-      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
-                       s/--\@\]/$delimiters{$format}[1]/g;
-                       $_;
-                     } 
-                 &$convert_map( $conf->config($inc_file) );
+sub _items_previous {
 
-    }
+  my $self = shift;
 
-    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";
+  # simple memoize
+  if ($self->get('_items_previous')) {
+    return sort { $a->{_date} <=> $b->{_date} }
+         values %{ $self->get('_items_previous') };
+  }
 
-    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;
-    }
+  # Gets the customer's current balance and outstanding invoices.
+  my ($prev_balance, @open_invoices) = $self->previous;
 
-    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+  my %invoices = map {
+    $_->invnum => $self->__items_previous_map_invoice($_)
+  } @open_invoices;
 
-    $invoice_data{$include} =~ s/\n+$//
-      if ($format eq 'latex');
-  }
+  # 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();
 
-  $invoice_data{'po_line'} =
-    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
-      ? &$escape_function("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 ',
-                            'html'     => $conf->config('money_char') || '$',
-                            'template' => '',
-                          );
-  my $other_money_char = $other_money_chars{$format};
-
-  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;
-  
-  my $previous_section = { 'description' => 'Previous Charges',
-                           'subtotal'    => $other_money_char.
-                                            sprintf('%.2f', $pr_total),
-                         };
-
-  my $taxtotal = 0;
-  my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
-                      'subtotal'    => $taxtotal }; # adjusted below
-
-  my $adjusttotal = 0;
-  my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
-                         'subtotal'    => 0 }; # adjusted below
-
-  my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
-  my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
-  my $late_sections = [];
-  if ( $multisection ) {
-    push @sections, $self->_items_sections( $late_sections );
-  }else{
-    push @sections, { 'description' => '', 'subtotal' => '' };
-  }
 
-  foreach my $line_item ( $conf->exists('disable_previous_balance') 
-                            ? ()
-                            : $self->_items_previous
-                        )
-  {
-    my $detail = {
-      ext_description => [],
-    };
-    $detail->{'ref'} = $line_item->{'pkgnum'};
-    $detail->{'quantity'} = 1;
-    $detail->{'section'} = $previous_section;
-    $detail->{'description'} = &$escape_function($line_item->{'description'});
-    if ( exists $line_item->{'ext_description'} ) {
-      @{$detail->{'ext_description'}} = map {
-        &$escape_function($_);
-      } @{$line_item->{'ext_description'}};
-    }
-    $detail->{'amount'} = ( $old_latex ? '' : $money_char).
-                          $line_item->{'amount'};
-    $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-  
-    push @detail_items, $detail;
-    push @buf, [ $detail->{'description'},
-                 $money_char. sprintf("%10.2f", $line_item->{'amount'}),
-               ];
-  }
-  
-  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
-    push @buf, ['','-----------'];
-    push @buf, [ 'Total Previous Balance',
-                 $money_char. sprintf("%10.2f", $pr_total) ];
-    push @buf, ['',''];
-  }
+  if ($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.
 
-  foreach my $section (@sections, @$late_sections) {
+    my @credits_postbill = $self->_items_credits_postbill();
+    my @payments_postbill = $self->_items_payments_postbill();
 
-    $section->{'subtotal'} = $other_money_char.
-                             sprintf('%.2f', $section->{'subtotal'})
-      if $multisection;
+    my %pmnt_dupechk;
+    my %cred_dupechk;
 
-    if ( $section->{'description'} ) {
-      push @buf, ( [ &$escape_function($section->{'description'}), '' ],
-                   [ '', '' ],
-                 );
-    }
+    # 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
+    #
 
-    my %options = ();
-    $options{'section'} = $section if $multisection;
-    $options{'format'} = $format;
-    $options{'escape_function'} = $escape_function;
-    $options{'format_function'} = sub { () } unless $unsquelched;
-    $options{'unsquelched'} = $unsquelched;
+    # 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;
 
-    foreach my $line_item ( $self->_items_pkg(%options) ) {
-      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'}};
+        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;
       }
-      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
-                              $line_item->{'amount'};
-      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
-                                 $line_item->{'unit_amount'};
-      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-  
-      push @detail_items, $detail;
-      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',
-                      $money_char. sprintf("%10.2f", $section->{'subtotal'})
-                   ],
-                   [ '', '' ],
-                   [ '', '' ],
-                 );
-    }
-  
-  }
-  
-  if ( $multisection && !$conf->exists('disable_previous_balance') ) {
-    unshift @sections, $previous_section if $pr_total;
-  }
+    # 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;
 
-  foreach my $tax ( $self->_items_tax ) {
-    my $total = {};
-    $total->{'total_item'} = &$escape_function($tax->{'description'});
-    $taxtotal += $tax->{'amount'};
-    $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
-    if ( $multisection ) {
-      my $money = $old_latex ? '' : $money_char;
-      push @detail_items, {
-        ext_description => [],
-        ref          => '',
-        quantity     => '',
-        description  => &$escape_function($tax->{'description'}),
-        amount       => $money. $tax->{'amount'},
-        product_code => '',
-        section      => $tax_section,
-      };
-    }else{
-      push @total_items, $total;
-    }
-    push @buf,[ $total->{'total_item'},
-                $money_char. sprintf("%10.2f", $total->{'total_amount'}),
-              ];
+        my $invnum = $cust_credit_bill->invnum;
 
-  }
-  
-  if ( $taxtotal ) {
-    my $total = {};
-    $total->{'total_item'} = '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,[( $conf->exists('disable_previous_balance') 
-               ? 'Total Charges'
-               : 'Total New Charges'
-             ),
-             $money_char. sprintf("%10.2f",$self->charged) ];
-  push @buf,['',''];
-
-  {
-    my $total = {};
-    $total->{'total_item'} = &$embolden_function('Total');
-    $total->{'total_amount'} =
-      &$embolden_function(
-        $other_money_char.
-        sprintf( '%.2f',
-                 $self->charged + ( $conf->exists('disable_previous_balance')
-                                    ? 0
-                                    : $pr_total
-                                  )
-               )
-      );
-    if ( $multisection ) {
-      $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
-                                      sprintf('%.2f', $self->charged );
-    }else{
-      push @total_items, $total;
-    }
-    push @buf,['','-----------'];
-    push @buf,['Total Charges',
-               $money_char.
-               sprintf( '%10.2f', $self->charged +
-                                    ( $conf->exists('disable_previous_balance')
-                                        ? 0
-                                        : $pr_total
-                                    )
-                      )
-              ];
-    push @buf,['',''];
-  }
-  
-  unless ( $conf->exists('disable_previous_balance') ) {
-    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-  
-    # credits
-    my $credittotal = 0;
-    foreach my $credit ( $self->_items_credits ) {
-      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 ( $self->cust_credited ) {
-  
-      #something more elaborate if $_->amount ne $_->cust_credit->credited ?
-
-      my $reason = substr($_->cust_credit->reason,0,32);
-      $reason .= '...' if length($reason) < length($_->cust_credit->reason);
-      $reason = " ($reason) " if $reason;
-      push @buf,[
-        "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
-        $money_char. sprintf("%10.2f",$_->amount)
-      ];
-    }
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
 
-    # 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;
+        $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
       }
-      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;
     }
 
-    { 
-      my $total;
-      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
-      $total->{'total_amount'} =
-        &$embolden_function(
-          $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
-        );
-      if ( $multisection ) {
-        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
-                                         $total->{'total_amount'};
-      }else{
-        push @total_items, $total;
-      }
-      push @buf,['','-----------'];
-      push @buf,[$self->balance_due_msg, $money_char. 
-        sprintf("%10.2f", $balance_due ) ];
-    }
-  }
+    # 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 ( $multisection ) {
-    push @sections, @$late_sections
-      if $unsquelched;
-  }
+      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";
+      }
 
-  $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 );
+      my $invnum = $postbill->{invnum};
 
-  if ($format eq 'template') {
+      $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+        unless exists $invoices{$invnum};
 
-    if ( $invoice_lines ) {
-      $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
-      $invoice_data{'total_pages'}++
-        if scalar(@buf) % $invoice_lines;
+      $invoices{$invnum}->{amount} += $postbill->{amount};
     }
 
-    #setup subroutine for the template
-    sub FS::cust_bill::_template::invoice_lines {
-      my $lines = shift || scalar(@FS::cust_bill::_template::buf);
-      map { 
-        scalar(@FS::cust_bill::_template::buf)
-          ? shift @FS::cust_bill::_template::buf
-          : [ '', '' ];
-      }
-      ( 1 .. $lines );
-    }
+    # Make sure current invoice doesn't appear in previous items
+    delete $invoices{$self->invnum}
+      if exists $invoices{$self->invnum};
 
-    my $lines;
-    my @collect;
-    while (@buf) {
-      push @collect, split("\n",
-        $text_template->fill_in( HASH => \%invoice_data,
-                                 PACKAGE => 'FS::cust_bill::_template'
-                               )
-      );
-      $FS::cust_bill::_template::page++;
-    }
-    map "$_\n", @collect;
-  }else{
-    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);
   }
-}
 
-=item print_ps [ TIME [ , TEMPLATE ] ]
+  # 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;
 
-Returns an postscript invoice, as a scalar.
+  $self->set('_items_previous', \%invoices);
+  return sort { $a->{_date} <=> $b->{_date} } values %invoices;
 
-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.
+}
 
-=cut
+=item _items_previous_total
 
-sub print_ps {
-  my $self = shift;
+  Return sum of amounts from all items returned by _items_previous
+  Results will vary based on invoicing conf flags
 
-  my ($file, $lfile) = $self->print_latex(@_);
-  my $ps = generate_ps($file);
-  unlink($lfile);
+=cut
 
-  $ps;
+sub _items_previous_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_previous();
+  return $tot;
+}
+
+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 $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
+  return $self->__items_previous_map_invoice($cust_bill);
+}
+
+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)"
+    ),
+  }
 }
 
-=item print_pdf [ TIME [ , TEMPLATE ] ]
+=item _items_credits()
 
-Returns an PDF invoice, as a scalar.
+  Return array of hashrefs containing credits to be shown as line-items
+  when rendering this bill.
 
-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.
+  keys for each credit item:
+  - crednum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
 
-=cut
+  This method has three ways it selects which credits to display on
+  this bill:
 
-sub print_pdf {
-  my $self = shift;
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
 
-  my ($file, $lfile) = $self->print_latex(@_);
-  my $pdf = generate_pdf($file);
-  unlink($lfile);
+     Returns credits that have been applied to this bill only
 
-  $pdf;
-}
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
 
-=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
+     List all credits that have been recorded during the time period
+     between the timestamps of the last invoice and this invoice
 
-Returns an HTML invoice, as a scalar.
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
 
-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.
+    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.
 
-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.
+     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
 
-sub print_html {
-  my $self = shift;
-  my %params;
-  if ( ref $_[0]  ) {
-    %params = %{ shift() }; 
-  }else{
-    $params{'time'} = shift;
-    $params{'template'} = shift;
-    $params{'cid'} = shift;
-  }
+sub _items_credits {
 
-  $params{'format'} = 'html';
+  my $self= shift;
 
-  $self->print_generic( %params );
-}
+  # Simple memoize
+  return @{$self->get('_items_credits')} if $self->get('_items_credits');
 
-# 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;
-}
-
-#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, '}',
-                      '--@]';
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+  my $trim_len = $opt{template} || $self->get('trim_len') || 40;
 
-    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+  my @return;
+  my @cust_credit_objs;
 
-      push @template, '[@--',
-                      '  foreach my $_tr_line (@total_items) {';
+  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
 
-      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';";
-      }
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
 
-      push @template, '}',
-                      '--@]';
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
+
+      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 {
-      $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
-      push @template, $line;  
-    }
-  
-  }
+      # Case 2 (see above)
+      # Return credits timestamps between this and the previous bills
 
-  if ($DEBUG) {
-    warn "$_\n" foreach @template;
-  }
+      my $date_start = 0;
+      my $date_end = $self->_date;
 
-  (@template);
-}
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
 
-sub terms {
-  my $self = shift;
+      @cust_credit_objs = qsearch({
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
+      });
+    }
 
-  #check for an invoice- specific override (eventually)
-  
-  #check for a customer- specific override
-  return $self->cust_main->invoice_terms
-    if $self->cust_main->invoice_terms;
+  } else {
+    # Case 1 (see above)
+    # Return only credits that have been applied to this bill
 
-  #use configured default or default default
-  $conf->config('invoice_default_terms') || 'Payable upon receipt';
-}
+    @cust_credit_objs = $self->cust_credited;
 
-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) : '';
-}
+  # 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,
+    );
 
-sub balance_due_msg {
-  my $self = shift;
-  my $msg = 'Balance Due';
-  return $msg unless $self->terms;
-  if ( $self->due_date ) {
-    $msg .= ' - Please pay by '. $self->due_date2str('%x');
-  } elsif ( $self->terms ) {
-    $msg .= ' - '. $self->terms;
-  }
-  $msg;
-}
+    my $reason = substr($cust_credit->reason, 0, $trim_len);
+    $reason .= '...' if length($reason) < length($cust_credit->reason);
+    $reason = "($reason)" if $reason;
 
-sub balance_due_date {
-  my $self = shift;
-  my $duedate = '';
-  if (    $conf->exists('invoice_default_terms') 
-       && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+    $r_obj{description} = join(' ',
+      $self->mt('Credit applied'),
+      $self->time2str_local('short', $cust_credit->_date),
+      $reason,
+    );
+
+    push @return, \%r_obj;
   }
-  $duedate;
+  $self->set('_items_credits',\@return);
+  @return;
 }
 
-=item invnum_date_pretty
+=item _items_credits_total
 
-Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
+  Return the total of al items from _items_credits
+  Will vary based on invoice display conf flag
 
 =cut
 
-sub invnum_date_pretty {
+sub _items_credits_total {
   my $self = shift;
-  'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_credits();
+  return $tot;
 }
 
-sub _items_sections {
-  my $self = shift;
-  my $late = shift;
 
-  my %s = ();
-  my %l = ();
 
-  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
-  {
+=item _items_credits_postbill()
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+  Returns an array of hashrefs for credits where
+  - Credit issued after this invoice
+  - Credit applied to an invoice before this invoice
 
-      my $desc = $cust_bill_pkg->section;
-      my $dup_desc = $cust_bill_pkg->duplicate_section;
+  Returned hashrefs are of the format returned by _items_credits()
 
-      if ($cust_bill_pkg->duplicate) {
-        $s{$dup_desc} += $cust_bill_pkg->setup
-          if ( $cust_bill_pkg->setup != 0 );
+=cut
 
-        $s{$dup_desc} += $cust_bill_pkg->recur
-          if ( $cust_bill_pkg->recur != 0 );
-      }
+sub _items_credits_postbill {
+  my $self = shift;
 
-      if ( $cust_bill_pkg->post_total ) {
-        $l{$desc} += $cust_bill_pkg->setup
-          if ( $cust_bill_pkg->setup != 0 );
+  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},
+#    },
+  });
 
-        $l{$desc} += $cust_bill_pkg->recur
-          if ( $cust_bill_pkg->recur != 0 );
+  return map {{
+    _date         => $_->_date,
+    invnum        => $_->invnum,
+    amount        => $_->amount,
+    creditbillnum => $_->creditbillnum,
+  }} @cust_credit_bill;
+}
 
-      } else {
-        $s{$desc} += $cust_bill_pkg->setup
-          if ( $cust_bill_pkg->setup != 0 );
+=item _items_payments_postbill()
 
-        $s{$desc} += $cust_bill_pkg->recur
-          if ( $cust_bill_pkg->recur != 0 );
-      }
+  Returns an array of hashrefs for payments where
+  - Payment occured after this invoice
+  - Payment applied to an invoice before this invoice
 
-    }
+  Returned hashrefs are of the format returned by _items_payments()
 
-  }
+=cut
 
-  push @$late, map { { 'description' => $_,
-                       'subtotal'    => $l{$_},
-                       'post_total'  => 1,
-                   } } sort keys %l;
+sub _items_payments_postbill {
+  my $self = shift;
 
-  map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
+  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.' ',
+  });
 
+  return map {{
+    _date      => $_->_date,
+    invnum     => $_->invnum,
+    amount     => $_->amount,
+    billpaynum => $_->billpaynum,
+  }} @cust_bill_pay;
 }
 
-sub _items {
-  my $self = shift;
+=item _items_payments()
 
-  #my @display = scalar(@_)
-  #              ? @_
-  #              : qw( _items_previous _items_pkg );
-  #              #: qw( _items_pkg );
-  #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
-  my @display = qw( _items_previous _items_pkg );
+  Return array of hashrefs containing payments to be shown as line-items
+  when rendering this bill.
 
-  my @b = ();
-  foreach my $display ( @display ) {
-    push @b, $self->$display(@_);
-  }
-  @b;
-}
+  keys for each payment item:
+  - paynum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
 
-sub _items_previous {
-  my $self = shift;
-  my $cust_main = $self->cust_main;
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-  my @b = ();
-  foreach ( @pr_cust_bill ) {
-    push @b, {
-      'description' => 'Previous Balance, Invoice #'. $_->invnum. 
-                       ' ('. time2str('%x',$_->_date). ')',
-      #'pkgpart'     => 'N/A',
-      'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%.2f", $_->owed),
-    };
-  }
-  @b;
+  This method has three ways it selects which payments to display on
+  this bill:
 
-  #{
-  #    '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 ],
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
 
-  #};
-}
+     Returns payments that have been applied to this bill only
 
-sub _items_pkg {
-  my $self = shift;
-  my %options = @_;
-  my $section = $options{'section'};
-  my $desc = $section->{'description'};
-  my @cust_bill_pkg =
-    grep { $_->pkgnum &&
-           ( defined($section)
-               ? ( $_->section eq $desc || $_->duplicate_section eq $desc )
-               : 1
-           )
-         } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
-}
-
-sub _taxsort {
-  return 0 unless $a cmp $b;
-  return -1 if $b eq 'Tax';
-  return 1 if $a eq 'Tax';
-  return -1 if $b eq 'Other surcharges';
-  return 1 if $a eq 'Other surcharges';
-  $a cmp $b;
-}
-
-sub _items_tax {
-  my $self = shift;
-  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
-  $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
-}
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
+
+     List all payments that have been recorded between the timestamps
+     of the previous 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.
+
+     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.
+
+=cut
+
+sub _items_payments {
 
-sub _items_cust_bill_pkg {
   my $self = shift;
-  my $cust_bill_pkg = shift;
-  my %opt = @_;
 
-  my $format = $opt{format} || '';
-  my $escape_function = $opt{escape_function} || sub { shift };
-  my $format_function = $opt{format_function} || '';
-  my $unsquelched = $opt{unsquelched} || '';
+  # Simple memoize
+  return @{$self->get('_items_payments')} if $self->get('_items_payments');
 
-  my @b = ();
-  my $last_pkgnum = '';
-  foreach my $cust_bill_pkg ( @$cust_bill_pkg )
-  {
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
 
-    my $cust_pkg = $cust_bill_pkg->cust_pkg;
+  my @return;
+  my @cust_pay_objs;
 
-    my $desc = $cust_bill_pkg->desc;
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
 
-    my %details_opt = ( 'format'          => $format,
-                        'escape_function' => $escape_function,
-                        'format_function' => $format_function,
-                      );
+  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
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
 
-      if ( $cust_bill_pkg->setup != 0 ) {
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
 
-        my $description = $desc;
-        $description .= ' Setup' if $cust_bill_pkg->recur != 0;
+      my %query = (
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
 
-        my @d = map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short($self->_date);
-        push @d, $cust_bill_pkg->details(%details_opt)
-          if $cust_bill_pkg->recur == 0;
+      @cust_pay_objs = qsearch(\%query);
 
-        push @b, {
-          description     => $description,
-          #pkgpart         => $part_pkg->pkgpart,
-          pkgnum          => $cust_bill_pkg->pkgnum,
-          amount          => sprintf("%.2f", $cust_bill_pkg->setup),
-          unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
-          quantity        => $cust_bill_pkg->quantity,
-          ext_description => \@d,
-        };
+    } else {
+      # Case 2 (see above)
+      # Return payments timestamped between this and the previous bill
 
-        $last_pkgnum = '';
+      my $date_start = 0;
+      my $date_end = $self->_date;
 
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
       }
 
-      if ( $cust_bill_pkg->recur != 0 ) {
+      @cust_pay_objs = qsearch({
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
+      });
+    }
 
-        my $is_summary =
-          ( $cust_bill_pkg->duplicate && 
-            $opt{section}->{description} ne $cust_bill_pkg->section
-          );
-        my $description = $is_summary ? "Usage charges" : $desc;
+  } else {
+    # Case 1 (see above)
+    # Return payments applied only to this bill
 
-        unless ( $conf->exists('disable_line_item_date_ranges') ) {
-          $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
-                          " - ". time2str("%x", $cust_bill_pkg->edate). ")";
-        }
+    @cust_pay_objs = $self->cust_bill_pay;
 
-        #at least until cust_bill_pkg has "past" ranges in addition to
-        #the "future" sdate/edate ones... see #3032
-        my @d = ();
-        push @d, map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short($self->_date)
-                                              #$cust_bill_pkg->edate,
-                                              #$cust_bill_pkg->sdate),
-          unless ($cust_bill_pkg->pkgnum eq $last_pkgnum);
-
-        @d = () if ($cust_bill_pkg->itemdesc || $is_summary);
-        push @d, $cust_bill_pkg->details(%details_opt)
-          unless $is_summary;
-
-        if ($cust_bill_pkg->pkgnum eq $last_pkgnum) {
-
-          $b[$#b]->{amount} =
-            sprintf("%.2f", $b[$#b]->{amount} + $cust_bill_pkg->recur);
-          push @{$b[$#b]->{ext_description}}, @d;
-
-        }else{
-
-          push @b, {
-            description     => $description,
-            #pkgpart         => $part_pkg->pkgpart,
-            pkgnum          => $cust_bill_pkg->pkgnum,
-            amount          => sprintf("%.2f", $cust_bill_pkg->recur),
-            unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
-            quantity        => $cust_bill_pkg->quantity,
-            ext_description => \@d,
-          };
+  }
 
-        }
+  $self->set(
+    '_items_payments',
+    [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+  );
+  return @{ $self->get('_items_payments') };
+}
 
-        if ($conf->exists('separate_usage') && $cust_bill_pkg->type ne 'U') {
-          $last_pkgnum = '';
-        }else{
-          $last_pkgnum = $cust_bill_pkg->pkgnum;
-        }
-      }
+=item _items_payments_total
 
-    } else { #pkgnum tax or one-shot line item (??)
+  Return a total of all records returned by _items_payments
+  Results vary based on invoicing conf flags
 
-      if ( $cust_bill_pkg->setup != 0 ) {
-        push @b, {
-          'description' => $desc,
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
-        };
-      }
-      if ( $cust_bill_pkg->recur != 0 ) {
-        push @b, {
-          'description' => "$desc (".
-                           time2str("%x", $cust_bill_pkg->sdate). ' - '.
-                           time2str("%x", $cust_bill_pkg->edate). ')',
-          'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
-        };
-      }
+=cut
 
-      $last_pkgnum = '';
+sub _items_payments_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_payments();
+  return $tot;
+}
+
+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;
+
+  for my $obj (@cust_pay_objs) {
+
+    # 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 %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),
+      ),
+    );
 
+    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;
+}
+
+=item _items_total()
+
+  Generate the line-items to be shown on the bill in the "Totals" section
+
+  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
 
-  @b;
+sub _items_total {
+  my $self = shift;
+  my $conf = $self->conf;
 
-}
+  my $c_multi_line_total = 0;
+  $c_multi_line_total    = 1
+    if $conf->exists('previous_balance-exclude_from_total')
+    && $self->enable_previous();
 
-sub _items_credits {
-  my $self = shift;
+  my @line_items;
+  my $invoice_charges  = $self->charged();
+
+  # _items_previous() is aware of conf flags
+  my $previous_balance = 0;
+  $previous_balance += $_->{amount} for $self->_items_previous();
 
-  my @b;
-  #credits
-  foreach ( $self->cust_credited ) {
-
-    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
-
-    my $reason = $_->cust_credit->reason;
-    #my $reason = substr($_->cust_credit->reason,0,32);
-    #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
-    $reason = " ($reason) " if $reason;
-    push @b, {
-      #'description' => 'Credit ref\#'. $_->crednum.
-      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
-      #                 $reason,
-      'description' => 'Credit applied '.
-                       time2str("%x",$_->cust_credit->_date). $reason,
-      'amount'      => sprintf("%.2f",$_->amount),
+  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');
   }
-  #foreach ( @cr_cust_credit ) {
-  #  push @buf,[
-  #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
-  #    $money_char. sprintf("%10.2f",$_->credited)
-  #  ];
-  #}
 
-  @b;
+  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)
+      );
+    }
+  }
 
+  push @line_items, {
+    total_amount => sprintf('%.2f', $total_charges),
+    total_item   => $total_descr,
+  };
+
+  return @line_items;
 }
 
-sub _items_payments {
+=item _items_aging_balances
+
+  Returns an array of aged balance amounts from a given epoch timestamp.
+
+  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.
+
+  Will not include any balances dated after the given timestamp in
+  the calculated totals
+
+  usage:
+  @aged_balances = $b->_items_aging_balances( $b->_date )
+
+  @aged_balances = (
+    under30d,
+    30d-60d,
+    60d-90d,
+    over90d
+  )
+
+=cut
+
+sub _items_aging_balances {
+  my ($self, $basetime) = @_;
+  die "Incorrect usage of _items_aging_balances()" unless ref $self;
+
+  $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
+
+  # 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();
+
+  # set our epoch breakpoints
+  $_ = $dt_12am - $_ for $d30, $d60, $d90;
+
+  # 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};
+    }
+  }
+
+  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 ... ]
+
+Returns an array of CSV strings representing the call details for this invoice
+The only option available is the boolean prepend_billed_number
+
+=cut
+
+sub call_details {
+  my ($self, %opt) = @_;
 
-  my @b;
-  #get & print payments
-  foreach ( $self->cust_bill_pay ) {
+  my $format_function = sub { shift };
 
-    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+  if ($opt{prepend_billed_number}) {
+    $format_function = sub {
+      my $detail = shift;
+      my $row = shift;
+
+      $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
 
-    push @b, {
-      'description' => "Payment received ".
-                       time2str("%x",$_->cust_pay->_date ),
-      'amount'      => sprintf("%.2f", $_->amount )
     };
   }
 
-  @b;
-
+  my @details = map { $_->details( 'format_function' => $format_function,
+                                   'escape_function' => sub{ return() },
+                                 )
+                    }
+                  grep { $_->pkgnum }
+                  $self->cust_bill_pkg;
+  my $header = $details[0];
+  ( $header, grep { $_ ne $header } @details );
 }
 
+=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
 
@@ -2887,7 +3616,7 @@ sub _items_payments {
 
 =over 4
 
-=item reprint
+=item process_reprint
 
 =cut
 
@@ -2895,7 +3624,7 @@ sub process_reprint {
   process_re_X('print', @_);
 }
 
-=item reemail
+=item process_reemail
 
 =cut
 
@@ -2903,7 +3632,7 @@ sub process_reemail {
   process_re_X('email', @_);
 }
 
-=item refax
+=item process_refax
 
 =cut
 
@@ -2911,7 +3640,7 @@ sub process_refax {
   process_re_X('fax', @_);
 }
 
-=item reftp
+=item process_reftp
 
 =cut
 
@@ -2919,14 +3648,20 @@ sub process_reftp {
   process_re_X('ftp', @_);
 }
 
-use Storable qw(thaw);
+=item respool
+
+=cut
+
+sub process_respool {
+  process_re_X('spool', @_);
+}
+
 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;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   re_X(
@@ -2937,7 +3672,11 @@ 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 ) = @_;
   if ( $DEBUG ) {
     warn "re_X $method for job $job with param:\n".
@@ -2945,22 +3684,15 @@ sub re_X {
   }
 
   #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(\%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';
 
@@ -2986,6 +3718,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
@@ -2999,8 +3739,10 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 =cut
 
 sub owed_sql {
-  my $class = shift;
-  'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '.
+    $class->paid_sql($start, $end). ' - '.
+    $class->credited_sql($start, $end);
 }
 
 =item net_sql
@@ -3010,8 +3752,8 @@ Returns an SQL fragment to retreive the net amount (charged minus credited).
 =cut
 
 sub net_sql {
-  my $class = shift;
-  'charged - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '. $class->credited_sql($start, $end);
 }
 
 =item paid_sql
@@ -3021,9 +3763,13 @@ Returns an SQL fragment to retreive the amount paid against this invoice.
 =cut
 
 sub paid_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_bill_pay._date <= $start";
+  $end   &&= "AND cust_bill_pay._date > $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
-       WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
+       WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
 }
 
 =item credited_sql
@@ -3033,122 +3779,40 @@ Returns an SQL fragment to retreive the amount credited against this invoice.
 =cut
 
 sub credited_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_credit_bill._date <= $start";
+  $end   &&= "AND cust_credit_bill._date >  $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
-       WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
+       WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
 }
 
-=item search_sql HASHREF
-
-Class method which returns an SQL WHERE fragment to search for parameters
-specified in HASHREF.  Valid parameters are
-
-=over 4
+=item due_date_sql
 
-=item begin
-
-Epoch date (UNIX timestamp) setting a lower bound for _date values
-
-=item end
-
-Epoch date (UNIX timestamp) setting an upper bound for _date values
-
-=item invnum_min
-
-=item invnum_max
-
-=item agentnum
-
-=item owed
-
-=item net
-
-=item days
-
-=item newest_percust
-
-=back
-
-Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
+Returns an SQL fragment to retrieve the due date of an invoice.
+Currently only supported on PostgreSQL.
 
 =cut
 
-sub search_sql {
-  my($class, $param) = @_;
-  if ( $DEBUG ) {
-    warn "$me search_sql called with params: \n".
-         join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
-  }
-
-  my @search = ();
-
-  if ( $param->{'begin'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill._date >= $1";
-  }
-  if ( $param->{'end'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill._date < $1";
-  }
-  if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.invnum >= $1";
-  }
-  if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.invnum <= $1";
-  }
-  if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_main.agentnum = $1";
-  }
-
-  push @search, '0 != '. FS::cust_bill->owed_sql
-    if $param->{'open'};
-
-  push @search, '0 != '. FS::cust_bill->net_sql
-    if $param->{'net'};
-
-  push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
-    if $param->{'days'};
+sub due_date_sql {
+  die "don't use: doesn't account for agent-specific invoice_default_terms";
 
-  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
-    )";
-
-  }
-
-  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 );
+  #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(
+    COALESCE(
+      cust_bill.invoice_terms,
+      cust_main.invoice_terms,
+      \''.($conf->config('invoice_default_terms') || '').'\'
+    ), E\'Net (\\\\d+)\'
+  )::INTEGER, 0
+) * 86400 + cust_bill._date'
 }
 
 =back
@@ -3166,4 +3830,3 @@ documentation.
 =cut
 
 1;
-