RT# 78547 - Flag to disable email/print/fax/etc during tests or reports
[freeside.git] / FS / FS / cust_bill.pm
index 4e34ef4..47f71c4 100644 (file)
@@ -1,21 +1,24 @@
 package FS::cust_bill;
 package FS::cust_bill;
-use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
+use base qw( FS::cust_bill::Search FS::Template_Mixin
+             FS::cust_main_Mixin FS::Record
+           );
 
 use strict;
 
 use strict;
-use vars qw( $DEBUG $me $date_format );
+use vars qw( $DEBUG $me );
              # but NOT $conf
              # but NOT $conf
+use Carp;
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
 use Date::Format;
+use DateTime;
 use File::Temp 0.14;
 use HTML::Entities;
 use Storable qw( freeze thaw );
 use GD::Barcode;
 use FS::UID qw( datasrc );
 use File::Temp 0.14;
 use HTML::Entities;
 use Storable qw( freeze thaw );
 use GD::Barcode;
 use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax do_print );
+use FS::Misc qw( send_fax do_print );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::Record qw( qsearch qsearchs dbh );
-use FS::cust_main;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
 use FS::cust_statement;
 use FS::cust_bill_pkg;
 use FS::cust_bill_pkg_display;
@@ -25,13 +28,9 @@ use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
-use FS::cust_pay_batch;
-use FS::cust_bill_event;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
-use FS::cust_bill_pay_batch;
-use FS::part_bill_event;
 use FS::payby;
 use FS::bill_batch;
 use FS::cust_bill_batch;
 use FS::payby;
 use FS::bill_batch;
 use FS::cust_bill_batch;
@@ -39,17 +38,13 @@ use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
 use FS::discount_plan;
 use FS::cust_bill_void;
 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]';
 
 use FS::L10N;
 
 $DEBUG = 0;
 $me = '[FS::cust_bill]';
 
-#ask FS::UID to run this stuff for us later
-FS::UID->install_callback( sub { 
-  my $conf = new FS::Conf; #global
-  $date_format      = $conf->config('date_format')      || '%x'; #/YY
-} );
-
 =head1 NAME
 
 FS::cust_bill - Object methods for cust_bill records
 =head1 NAME
 
 FS::cust_bill - Object methods for cust_bill records
@@ -106,23 +101,19 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =back
 
 
 =back
 
-Customer info at invoice generation time
+Deprecated fields
 
 =over 4
 
 
 =over 4
 
-=item billing_balance - the customer's balance at the time the invoice was 
-generated (not including charges on this invoice)
-
-=item previous_balance - the billing_balance of this customer's previous 
-invoice plus the charges on that invoice
-
-=back
-
-Deprecated
+=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.
 
 
-=over 4
+=item previous_balance - the customer's balance immediately after generating
+the invoice before this one.  DEPRECATED.
 
 
-=item printed - deprecated
+=item printed - formerly used to track the number of times an invoice had
+been printed; no longer used.
 
 =back
 
 
 =back
 
@@ -138,6 +129,8 @@ Specific use cases
 
 =item promised_date - customer promised payment date, for collection
 
 
 =item promised_date - customer promised payment date, for collection
 
+=item pending - invoice is still being generated, empty or 'Y'
+
 =back
 
 =head1 METHODS
 =back
 
 =head1 METHODS
@@ -153,6 +146,7 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
 =cut
 
 sub table { 'cust_bill'; }
+sub template_conf { 'invoice_'; }
 
 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
 # (except email_subject and invnum_date_pretty)
 
 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
 # (except email_subject and invnum_date_pretty)
@@ -161,7 +155,7 @@ sub notice_name {
   $self->conf->config('notice_name') || 'Invoice'
 }
 
   $self->conf->config('notice_name') || 'Invoice'
 }
 
-sub cust_linked { $_[0]->cust_main_custnum; } 
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum }
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -212,7 +206,7 @@ sub insert {
 
 }
 
 
 }
 
-=item void
+=item void [ REASON [ , REPROCESS_CDRS ] ]
 
 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
 
 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
@@ -223,6 +217,15 @@ FS::cust_bill_pkg_void).
 sub void {
   my $self = shift;
   my $reason = scalar(@_) ? shift : '';
 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{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
@@ -238,7 +241,7 @@ sub void {
   my $cust_bill_void = new FS::cust_bill_void ( {
     map { $_ => $self->get($_) } $self->fields
   } );
   my $cust_bill_void = new FS::cust_bill_void ( {
     map { $_ => $self->get($_) } $self->fields
   } );
-  $cust_bill_void->reason($reason);
+  $cust_bill_void->reasonnum($reason->reasonnum) if $reason;
   my $error = $cust_bill_void->insert;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
   my $error = $cust_bill_void->insert;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
@@ -246,14 +249,14 @@ sub void {
   }
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
   }
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
-    my $error = $cust_bill_pkg->void($reason);
+    my $error = $cust_bill_pkg->void($reason, $reprocess_cdrs);
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
   }
 
     if ( $error ) {
       $dbh->rollback if $oldAutoCommit;
       return $error;
     }
   }
 
-  $error = $self->delete;
+  $error = $self->_delete;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -265,20 +268,22 @@ sub void {
 
 }
 
 
 }
 
-=item delete
-
-This method now works but you probably shouldn't use it.  Instead, apply a
-credit against the invoice, or use the new void method.
-
-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.
-
-Really, don't use it.
-
-=cut
+# removed docs entirely and renamed method to _delete to further indicate it is
+# internal-only and discourage use
+#
+# =item delete
+#
+# DO NOT USE THIS METHOD.  Instead, apply a credit against the invoice, or use
+# the B<void> method.
+#
+# This is only for internal use by V<void>, which is what you should be using.
+#
+# DO NOT USE THIS METHOD.  Whatever reason you think you have is almost certainly
+# wrong.  Use B<void>, that's what it is for.  Really.  This means you.
+#
+# =cut
 
 
-sub delete {
+sub _delete {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
 
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
 
@@ -294,15 +299,14 @@ sub delete {
   my $dbh = dbh;
 
   foreach my $table (qw(
   my $dbh = dbh;
 
   foreach my $table (qw(
-    cust_bill_event
-    cust_event
     cust_credit_bill
     cust_credit_bill
-    cust_bill_pay
-    cust_pay_batch
     cust_bill_pay_batch
     cust_bill_pay_batch
+    cust_bill_pay
     cust_bill_batch
     cust_bill_pkg
   )) {
     cust_bill_batch
     cust_bill_pkg
   )) {
+    #cust_event # problematic
+    #cust_pay_batch # unnecessary
 
     foreach my $linked ( $self->$table() ) {
       my $error = $linked->delete;
 
     foreach my $linked ( $self->$table() ) {
       my $error = $linked->delete;
@@ -347,6 +351,7 @@ sub replace_check {
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   return "Can't change _date" unless $old->_date == $new->_date;
   return "Can't change charged" unless $old->charged == $new->charged
   #return "Can't change _date!" unless $old->_date eq $new->_date;
   return "Can't change _date" unless $old->_date == $new->_date;
   return "Can't change charged" unless $old->charged == $new->charged
+                                    || $old->pending eq 'Y'
                                     || $old->charged == 0
                                    || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
                                     || $old->charged == 0
                                    || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
@@ -401,6 +406,7 @@ sub check {
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
     || $self->ut_numbern('agent_invid') #varchar?
     || $self->ut_enum('closed', [ '', 'Y' ])
     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
     || $self->ut_numbern('agent_invid') #varchar?
+    || $self->ut_flag('pending')
   ;
   return $error if $error;
 
   ;
   return $error if $error;
 
@@ -420,8 +426,8 @@ cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
 
 sub display_invnum {
   my $self = shift;
 
 sub display_invnum {
   my $self = shift;
-  my $conf = $self->conf;
-  if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
+  if ( $self->agent_invid
+         && FS::Conf->new->exists('cust_bill-default_agent_invid') ) {
     return $self->agent_invid;
   } else {
     return $self->invnum;
     return $self->agent_invid;
   } else {
     return $self->invnum;
@@ -447,25 +453,50 @@ sub previous_bill {
   $self->get('previous_bill');
 }
 
   $self->get('previous_bill');
 }
 
+=item following_bill
+
+Returns the customer's invoice that follows this one
+
+=cut
+
+sub following_bill {
+  my $self = shift;
+  if (!$self->get('following_bill')) {
+    $self->set('following_bill', qsearchs({
+      table   => 'cust_bill',
+      hashref => {
+        custnum => $self->custnum,
+        invnum  => { op => '>', value => $self->invnum },
+      },
+      order_by => 'ORDER BY invnum ASC LIMIT 1',
+    }));
+  }
+  $self->get('following_bill');
+}
+
 =item previous
 
 =item previous
 
-Returns a list consisting of the total previous balance for this customer, 
+Returns a list consisting of the total previous balance for this customer,
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
 
 sub previous {
   my $self = shift;
 followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 =cut
 
 sub previous {
   my $self = shift;
-  my $total = 0;
-  my @cust_bill = sort { $a->_date <=> $b->_date }
-    grep { $_->owed != 0 }
-      qsearch( 'cust_bill', { 'custnum' => $self->custnum,
-                              #'_date'   => { op=>'<', value=>$self->_date },
-                              'invnum'   => { op=>'<', value=>$self->invnum },
-                            } ) 
-  ;
-  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
 }
 
 =item enable_previous
@@ -490,9 +521,17 @@ Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
 sub cust_bill_pkg {
   my $self = shift;
   qsearch(
 sub cust_bill_pkg {
   my $self = shift;
   qsearch(
-    { 'table'    => 'cust_bill_pkg',
+    { 
+      'select'    => 'cust_bill_pkg.*, pkg_category.categoryname',
+      'table'    => 'cust_bill_pkg',
+      'addl_from' => ' LEFT JOIN cust_pkg     USING ( pkgnum ) '.
+                     ' LEFT JOIN part_pkg     USING ( pkgpart ) '.
+                     ' LEFT JOIN pkg_class    USING ( classnum ) '.
+                     ' LEFT JOIN pkg_category USING ( categorynum ) ',
       'hashref'  => { 'invnum' => $self->invnum },
       'hashref'  => { 'invnum' => $self->invnum },
-      'order_by' => 'ORDER BY billpkgnum',
+      'order_by' => 'ORDER BY billpkgnum', #important?  otherwise we could use
+                                           # the AUTLOADED FK search.  or should
+                                           # that default to ORDER by the pkey?
     }
   );
 }
     }
   );
 }
@@ -570,32 +609,6 @@ sub open_cust_bill_pkg {
   @open;
 }
 
   @open;
 }
 
-=item cust_bill_event
-
-Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub cust_bill_event {
-  my $self = shift;
-  qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
-}
-
-=item num_cust_bill_event
-
-Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub num_cust_bill_event {
-  my $self = shift;
-  my $sql =
-    "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
-  $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
-  $sth->fetchrow_arrayref->[0];
-}
-
 =item cust_event
 
 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
 =item cust_event
 
 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
@@ -625,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 $sql =
     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
+  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql";
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
   $sth->fetchrow_arrayref->[0];
 }
@@ -634,11 +647,21 @@ sub num_cust_event {
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
+
+Returns a list: an empty list on success or a list of errors.
+
 =cut
 
 =cut
 
-sub cust_main {
+sub suspend {
   my $self = shift;
   my $self = shift;
-  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+
+  grep { $_->suspend(@_) }
+  grep {! $_->getfield('cancel') }
+  $self->cust_pkg;
+
 }
 
 =item cust_suspend_if_balance_over AMOUNT
 }
 
 =item cust_suspend_if_balance_over AMOUNT
@@ -660,55 +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
 
 
 =cut
 
-sub cust_credit {
-  use Carp;
-  croak "FS::cust_bill->cust_credit depreciated; see ".
-        "FS::cust_bill->cust_credit_bill";
-  #my $self = shift;
-  #my $total = 0;
-  #my @cust_credit = sort { $a->_date <=> $b->_date }
-  #  grep { $_->credited != 0 && $_->_date < $self->_date }
-  #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
-  #;
-  #foreach (@cust_credit) { $total += $_->credited; }
-  #$total, @cust_credit;
-}
-
-=item cust_pay
-
-Depreciated.  See the cust_bill_pay method.
+sub cancel {
+  my( $self, %opt ) = @_;
 
 
-#Returns all payments (see L<FS::cust_pay>) for this invoice.
+  warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+       join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+    if $DEBUG;
 
 
-=cut
+  return ( 'Access denied' )
+    unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
 
 
-sub cust_pay {
-  use Carp;
-  croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
-  #my $self = shift;
-  #sort { $a->_date <=> $b->_date }
-  #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
-  #;
-}
+  my @pkgs = $self->cust_pkg;
 
 
-sub cust_pay_batch {
-  my $self = shift;
-  qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
-}
+  if ( !$opt{nobill} && $self->conf->exists('bill_usage_on_cancel') ) {
+    $opt{nobill} = 1;
+    my $error = $self->cust_main->bill( pkg_list => [ @pkgs ], cancel => 1 );
+    warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
+      if $error;
+  }
 
 
-sub cust_bill_pay_batch {
-  my $self = shift;
-  qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
+  grep { $_ }
+    map { $_->cancel(%opt) }
+      grep { ! $_->getfield('cancel') }
+        @pkgs;
 }
 
 =item cust_bill_pay
 }
 
 =item cust_bill_pay
@@ -839,7 +842,7 @@ sub cust_bill_batch {
 
 =item discount_plans
 
 
 =item discount_plans
 
-Returns all discount plans (L<FS::discount_plan>) for this invoice, as a 
+Returns all discount plans (L<FS::discount_plan>) for this invoice, as a
 hash keyed by term length.
 
 =cut
 hash keyed by term length.
 
 =cut
@@ -882,6 +885,35 @@ sub owed {
   $balance;
 }
 
   $balance;
 }
 
+=item owed_on_invoice
+
+Returns the amount to be displayed as the "Balance Due" on this
+invoice.  Amount returned depends on conf flags for invoicing
+
+See L<FS::cust_bill::owed> for the true amount currently owed
+
+=cut
+
+sub owed_on_invoice {
+  my $self = shift;
+
+  #return $self->owed()
+  #  unless $self->conf->exists('previous_balance-payments_since')
+
+  # Add charges from this invoice
+  my $owed = $self->charged();
+
+  # Add carried balances from previous invoices
+  #   If previous items aren't to be displayed on the invoice,
+  #   _items_previous() is aware of this and responds appropriately.
+  $owed += $_->{amount} for $self->_items_previous();
+
+  # Subtract payments and credits displayed on this invoice
+  $owed -= $_->{amount} for $self->_items_payments(), $self->_items_credits();
+
+  return $owed;
+}
+
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
 sub owed_pkgnum {
   my( $self, $pkgnum ) = @_;
 
@@ -917,6 +949,7 @@ sub hide {
 =item apply_payments_and_credits [ OPTION => VALUE ... ]
 
 Applies unapplied payments and credits to this invoice.
 =item apply_payments_and_credits [ OPTION => VALUE ... ]
 
 Applies unapplied payments and credits to this invoice.
+Payments with the no_auto_apply flag set will not be applied.
 
 A hash of optional arguments may be passed.  Currently "manual" is supported.
 If true, a payment receipt is sent instead of a statement when
 
 A hash of optional arguments may be passed.  Currently "manual" is supported.
 If true, a payment receipt is sent instead of a statement when
@@ -943,7 +976,9 @@ sub apply_payments_and_credits {
 
   $self->select_for_update; #mutex
 
 
   $self->select_for_update; #mutex
 
-  my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
+  my @payments = grep { $_->unapplied > 0 }
+                   grep { !$_->no_auto_apply }
+                     $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
   if ( $conf->exists('pkg-balances') ) {
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
   if ( $conf->exists('pkg-balances') ) {
@@ -970,7 +1005,7 @@ sub apply_payments_and_credits {
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
           );
       my $max_credit_weight =
         max( map  { $_->part_pkg->credit_weight || 0 }
-            grep { $_ } 
+            grep { $_ }
              map  { $_->cust_pkg }
                   @open_lineitems
            );
              map  { $_->cust_pkg }
                   @open_lineitems
            );
@@ -981,7 +1016,7 @@ sub apply_payments_and_credits {
       } else {
         $app = 'credit';
       }
       } else {
         $app = 'credit';
       }
-    
+
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
     } elsif ( @payments ) {
       $app = 'pay';
     } elsif ( @credits ) {
@@ -1039,298 +1074,6 @@ sub apply_payments_and_credits {
 
 }
 
 
 }
 
-=item generate_email OPTION => VALUE ...
-
-Options:
-
-=over 4
-
-=item from
-
-sender address, required
-
-=item template
-
-alternate template name, optional
-
-=item print_text
-
-text attachment arrayref, optional
-
-=item subject
-
-email subject, optional
-
-=item notice_name
-
-notice name instead of "Invoice", optional
-
-=back
-
-Returns an argument list to be passed to L<FS::Misc::send_email>.
-
-=cut
-
-use MIME::Entity;
-
-sub generate_email {
-
-  my $self = shift;
-  my %args = @_;
-  my $conf = $self->conf;
-
-  my $me = '[FS::cust_bill::generate_email]';
-
-  my %return = (
-    'from'      => $args{'from'},
-    'subject'   => ($args{'subject'} || $self->email_subject),
-  );
-
-  $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
-
-  my $cust_main = $self->cust_main;
-
-  if (ref($args{'to'}) eq 'ARRAY') {
-    $return{'to'} = $args{'to'};
-  } else {
-    $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
-                           $cust_main->invoicing_list
-                    ];
-  }
-
-  if ( $conf->exists('invoice_html') ) {
-
-    warn "$me creating HTML/text multipart message"
-      if $DEBUG;
-
-    $return{'nobody'} = 1;
-
-    my $alternative = build MIME::Entity
-      'Type'        => 'multipart/alternative',
-      #'Encoding'    => '7bit',
-      'Disposition' => 'inline'
-    ;
-
-    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) ];
-      }
-
-    }
-
-    $alternative->attach(
-      'Type'        => 'text/plain',
-      'Encoding'    => 'quoted-printable',
-      #'Encoding'    => '7bit',
-      'Data'        => $data,
-      'Disposition' => 'inline',
-    );
-
-
-    my $htmldata;
-    my $image = '';
-    my $barcode = '';
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
-
-      $htmldata = join('<BR>', $conf->config('invoice_email_pdf_note') );
-
-    } else {
-
-      $args{'from'} =~ /\@([\w\.\-]+)/;
-      my $from = $1 || 'example.com';
-      my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-
-      my $logo;
-      my $agentnum = $cust_main->agentnum;
-      if ( defined($args{'template'}) && length($args{'template'})
-           && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
-         )
-      {
-        $logo = 'logo_'. $args{'template'}. '.png';
-      } else {
-        $logo = "logo.png";
-      }
-      my $image_data = $conf->config_binary( $logo, $agentnum);
-
-      $image = build MIME::Entity
-        'Type'       => 'image/png',
-        'Encoding'   => 'base64',
-        'Data'       => $image_data,
-        'Filename'   => 'logo.png',
-        'Content-ID' => "<$content_id>",
-      ;
-   
-      if ($conf->exists('invoice-barcode')) {
-        my $barcode_content_id = join('.', rand()*(2**32), $$, time). "\@$from";
-        $barcode = build MIME::Entity
-          'Type'       => 'image/png',
-          'Encoding'   => 'base64',
-          'Data'       => $self->invoice_barcode(0),
-          'Filename'   => 'barcode.png',
-          'Content-ID' => "<$barcode_content_id>",
-        ;
-        $args{'barcode_cid'} = $barcode_content_id;
-      }
-
-      $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
-    }
-
-    $alternative->attach(
-      'Type'        => 'text/html',
-      'Encoding'    => 'quoted-printable',
-      'Data'        => [ '<html>',
-                         '  <head>',
-                         '    <title>',
-                         '      '. encode_entities($return{'subject'}), 
-                         '    </title>',
-                         '  </head>',
-                         '  <body bgcolor="#e8e8e8">',
-                         $htmldata,
-                         '  </body>',
-                         '</html>',
-                       ],
-      'Disposition' => 'inline',
-      #'Filename'    => 'invoice.pdf',
-    );
-
-
-    my @otherparts = ();
-    if ( $cust_main->email_csv_cdr ) {
-
-      push @otherparts, build MIME::Entity
-        'Type'        => 'text/csv',
-        'Encoding'    => '7bit',
-        'Data'        => [ map { "$_\n" }
-                             $self->call_details('prepend_billed_number' => 1)
-                         ],
-        'Disposition' => 'attachment',
-        'Filename'    => 'usage-'. $self->invnum. '.csv',
-      ;
-
-    }
-
-    if ( $conf->exists('invoice_email_pdf') ) {
-
-      #attaching pdf too:
-      # multipart/mixed
-      #   multipart/related
-      #     multipart/alternative
-      #       text/plain
-      #       text/html
-      #     image/png
-      #   application/pdf
-
-      my $related = build MIME::Entity 'Type'     => 'multipart/related',
-                                       'Encoding' => '7bit';
-
-      #false laziness w/Misc::send_email
-      $related->head->replace('Content-type',
-        $related->mime_type.
-        '; boundary="'. $related->head->multipart_boundary. '"'.
-        '; type=multipart/alternative'
-      );
-
-      $related->add_part($alternative);
-
-      $related->add_part($image) if $image;
-
-      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
-
-      $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
-
-    } else {
-
-      #no other attachment:
-      # multipart/related
-      #   multipart/alternative
-      #     text/plain
-      #     text/html
-      #   image/png
-
-      $return{'content-type'} = 'multipart/related';
-      if ($conf->exists('invoice-barcode') && $barcode) {
-        $return{'mimeparts'} = [ $alternative, $image, $barcode, @otherparts ];
-      } else {
-        $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
-      }
-      $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
-      #$return{'disposition'} = 'inline';
-
-    }
-  
-  } else {
-
-    if ( $conf->exists('invoice_email_pdf') ) {
-      warn "$me creating PDF attachment"
-        if $DEBUG;
-
-      #mime parts arguments a la MIME::Entity->build().
-      $return{'mimeparts'} = [
-        { $self->mimebuild_pdf(\%args) }
-      ];
-    }
-  
-    if ( $conf->exists('invoice_email_pdf')
-         and scalar($conf->config('invoice_email_pdf_note')) ) {
-
-      warn "$me using 'invoice_email_pdf_note'"
-        if $DEBUG;
-      $return{'body'} = [ map { $_ . "\n" }
-                              $conf->config('invoice_email_pdf_note')
-                        ];
-
-    } 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) ];
-      }
-
-    }
-
-  }
-
-  %return;
-
-}
-
-=item mimebuild_pdf
-
-Returns a list suitable for passing to MIME::Entity->build(), representing
-this invoice as PDF attachment.
-
-=cut
-
-sub mimebuild_pdf {
-  my $self = shift;
-  (
-    'Type'        => 'application/pdf',
-    'Encoding'    => 'base64',
-    'Data'        => [ $self->print_pdf(@_) ],
-    'Disposition' => 'attachment',
-    'Filename'    => 'invoice-'. $self->invnum. '.pdf',
-  );
-}
-
 =item send HASHREF
 
 Sends this invoice to the destinations configured for this customer: sends
 =item send HASHREF
 
 Sends this invoice to the destinations configured for this customer: sends
@@ -1343,11 +1086,11 @@ I<template>: a suffix for alternate invoices
 
 I<agentnum>: obsolete, now does nothing.
 
 
 I<agentnum>: obsolete, now does nothing.
 
-I<invoice_from> overrides the default email invoice From: address.
+I<from> overrides the default email invoice From: address.
 
 I<amount>: obsolete, does nothing
 
 
 I<amount>: obsolete, does nothing
 
-I<notice_name> overrides "Invoice" as the name of the sent document 
+I<notice_name> overrides "Invoice" as the name of the sent document
 (templates from 10/2009 or newer required).
 
 I<lpr> overrides the system 'lpr' option as the command to print a document
 (templates from 10/2009 or newer required).
 
 I<lpr> overrides the system 'lpr' option as the command to print a document
@@ -1366,7 +1109,7 @@ sub send {
 
   $self->email($opt)
     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
 
   $self->email($opt)
     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
-    && ! $self->invoice_noemail;
+    && ! $cust_main->invoice_noemail;
 
   $self->print($opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
   $self->print($opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
@@ -1379,55 +1122,22 @@ sub send {
 
 }
 
 
 }
 
-=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
-
-Sends this invoice to the customer's email destination(s).
-
-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<invoice_from>, if specified, overrides the default email invoice From: 
-address.
-
-I<notice_name> is the name of the sent document.
-
-=cut
-
-sub queueable_email {
-  my %opt = @_;
-
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my %args = map {$_ => $opt{$_}} 
-             grep { $opt{$_} }
-              qw( invoice_from notice_name no_coupon template );
-
-  my $error = $self->email( \%args );
-  die $error if $error;
-
-}
-
 sub email {
   my $self = shift;
 sub email {
   my $self = shift;
-  return if $self->hide;
-  my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
   if ($opt and !ref($opt)) {
-    die "FS::cust_bill::email called with positional parameters";
+    die ref($self). '->email called with positional parameters';
   }
 
   }
 
-  my $template = $opt->{template};
-  my $from = delete $opt->{invoice_from};
+  my $conf = $self->conf;
+
+  my $from = delete $opt->{from};
 
   # this is where we set the From: address
   $from ||= $self->_agent_invoice_from ||    #XXX should go away
 
   # this is where we set the From: address
   $from ||= $self->_agent_invoice_from ||    #XXX should go away
-            $conf->config('invoice_from', $self->cust_main->agentnum );
+            $conf->invoice_from_full( $self->cust_main->agentnum );
 
 
-  my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
-                            $self->cust_main->invoicing_list;
+  my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
 
   if ( ! @invoicing_list ) { #no recipients
     if ( $conf->exists('cust_bill-no_recipients-error') ) {
 
   if ( ! @invoicing_list ) { #no recipients
     if ( $conf->exists('cust_bill-no_recipients-error') ) {
@@ -1438,19 +1148,31 @@ sub email {
     }
   }
 
     }
   }
 
-  # this is where we set the Subject:
-  my $subject = $self->email_subject($template);
+  $self->SUPER::email( {
+    'from' => $from,
+    'to'   => \@invoicing_list,
+    %$opt,
+  });
 
 
-  my $error = send_email(
-    $self->generate_email(
-      'from'        => $from,
-      'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
-      'subject'     => $subject,
-      %$opt, # template, etc.
-    )
-  );
-  die "can't email invoice: $error\n" if $error;
-  #die "$error\n" if $error;
+}
+
+#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};
+
+  $self->set('mode', $opt{mode})
+    if $opt{mode};
+
+  my %args = map {$_ => $opt{$_}}
+             grep { $opt{$_} }
+              qw( from notice_name no_coupon template );
+
+  my $error = $self->email( \%args );
+  die $error if $error;
 
 }
 
 
 }
 
@@ -1473,11 +1195,16 @@ sub email_subject {
   eval qq("$subject");
 }
 
   eval qq("$subject");
 }
 
+sub pdf_filename {
+  my $self = shift;
+  'Invoice-'. $self->invnum. '.pdf';
+}
+
 =item lpr_data HASHREF
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
 =item lpr_data HASHREF
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-Options must be passed as a hashref.  Positional parameters are no longer 
+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.
 allowed.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
@@ -1489,7 +1216,7 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 sub lpr_data {
   my $self = shift;
   my $conf = $self->conf;
 sub lpr_data {
   my $self = shift;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     # nobody does this anyway
     die "FS::cust_bill::lpr_data called with positional parameters";
   if ($opt and !ref($opt)) {
     # nobody does this anyway
     die "FS::cust_bill::lpr_data called with positional parameters";
@@ -1515,7 +1242,7 @@ sub print {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::print called with positional parameters";
   }
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::print called with positional parameters";
   }
@@ -1550,7 +1277,7 @@ sub fax_invoice {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-  my $opt = shift;
+  my $opt = shift || {};
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::fax_invoice called with positional parameters";
   }
   if ($opt and !ref($opt)) {
     die "FS::cust_bill::fax_invoice called with positional parameters";
   }
@@ -1570,9 +1297,11 @@ sub fax_invoice {
 
 =item batch_invoice [ HASHREF ]
 
 
 =item batch_invoice [ HASHREF ]
 
-Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there
 isn't an open batch, one will be created.
 
 isn't an open batch, one will be created.
 
+HASHREF may contain any options to be passed to C<print_pdf>.
+
 =cut
 
 sub batch_invoice {
 =cut
 
 sub batch_invoice {
@@ -1582,6 +1311,10 @@ sub batch_invoice {
       batchnum => $bill_batch->batchnum,
       invnum   => $self->invnum,
   });
       batchnum => $bill_batch->batchnum,
       invnum   => $self->invnum,
   });
+  if ( $self->mode ) {
+    $opt->{mode} ||= $self->mode;
+    $opt->{mode} = $opt->{mode}->modenum if ref $opt->{mode};
+  }
   return $cust_bill_batch->insert($opt);
 }
 
   return $cust_bill_batch->insert($opt);
 }
 
@@ -1608,7 +1341,7 @@ sub get_open_bill_batch {
   return $batch;
 }
 
   return $batch;
 }
 
-=item ftp_invoice [ TEMPLATENAME ] 
+=item ftp_invoice [ TEMPLATENAME ]
 
 Sends this invoice data via FTP.
 
 
 Sends this invoice data via FTP.
 
@@ -1631,7 +1364,7 @@ sub ftp_invoice {
   );
 }
 
   );
 }
 
-=item spool_invoice [ TEMPLATENAME ] 
+=item spool_invoice [ TEMPLATENAME ]
 
 Spools this invoice data (see L<FS::spool_csv>)
 
 
 Spools this invoice data (see L<FS::spool_csv>)
 
@@ -1672,14 +1405,20 @@ See L</print_csv> for a description of the output format.
 sub send_csv {
   my($self, %opt) = @_;
 
 sub send_csv {
   my($self, %opt) = @_;
 
+  if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+    warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+    return;
+  }
+
   #create file(s)
 
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
   #create file(s)
 
   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
   mkdir $spooldir, 0700 unless -d $spooldir;
 
+  # don't localize dates here, they're a defined format
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
   my $file = "$spooldir/$tracctnum.csv";
-  
+
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
 
   open(CSV, ">$file") or die "can't open $file: $!";
@@ -1747,6 +1486,11 @@ in the ICS format.
 sub spool_csv {
   my($self, %opt) = @_;
 
 sub spool_csv {
   my($self, %opt) = @_;
 
+  if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
+    warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
+    return;
+  }
+
   my $time = $opt{'time'} || time;
   my $cust_main = $self->cust_main;
 
   my $time = $opt{'time'} || time;
   my $cust_main = $self->cust_main;
 
@@ -1784,7 +1528,7 @@ sub spool_csv {
   }
 
   $file = "$spooldir/$file.csv";
   }
 
   $file = "$spooldir/$file.csv";
-  
+
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
 
   open(CSV, ">>$file") or die "can't open $file: $!";
@@ -1829,7 +1573,7 @@ detail information for this invoice.
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
 If I<format> is not specified or "default", the fields of the CSV file are as
 follows:
 
-record_type, invnum, custnum, _date, charged, first, last, company, address1, 
+record_type, invnum, custnum, _date, charged, first, last, company, address1,
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
 
 =over 4
@@ -1935,7 +1679,7 @@ If I<format> is "billco", the fields of the detail CSV file are as follows:
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
   9     | Grouping Code              | GROUP     | CHAR |     2
   10    | User Defined               | ACCT CODE | CHAR |    15
 
-If format is 'oneline', there is no detail file.  Each invoice has a 
+If format is 'oneline', there is no detail file.  Each invoice has a
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
 header line only, with the fields:
 
 Agent number, agent name, customer number, first name, last name, address
@@ -1945,21 +1689,21 @@ amount charged, amount due, previous balance, due date.
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
 and then, for each line item, three columns containing the package number,
 description, and amount.
 
-If format is 'bridgestone', there is no detail file.  Each invoice has a 
+If format is 'bridgestone', there is no detail file.  Each invoice has a
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
 header line with the following fields in a fixed-width format:
 
 Customer number (in display format), date, name (first last), company,
 address 1, address 2, city, state, zip.
 
 This is a mailing list format, and has no per-invoice fields.  To avoid
-sending redundant notices, the spooling event should have a "once" or 
+sending redundant notices, the spooling event should have a "once" or
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
 "once_percust_every" condition.
 
 =cut
 
 sub print_csv {
   my($self, %opt) = @_;
-  
+
   eval "use Text::CSV_XS";
   die $@ if $@;
 
   eval "use Text::CSV_XS";
   die $@ if $@;
 
@@ -1970,25 +1714,29 @@ sub print_csv {
 
   my $time = $opt{'time'} || time;
 
 
   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);
 
   if ( $format eq 'billco' ) {
 
     my $account_num =
       $self->conf->config('billco-account_num', $cust_main->agentnum);
 
-    my $tracctnum = $account_num eq 'display_custnum'
-                      ? $cust_main->display_custnum
-                      : $opt{'tracctnum'};
+    $tracctnum = $account_num eq 'display_custnum'
+                   ? $cust_main->display_custnum
+                   : $opt{'tracctnum'};
 
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
 
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
-    my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+    my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
-      foreach ( $self->_items_payments, $self->_items_credits ) ;
+      foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
@@ -2031,8 +1779,8 @@ sub print_csv {
     );
 
   } elsif ( $format eq 'oneline' ) { #name
     );
 
   } elsif ( $format eq 'oneline' ) { #name
-  
-    my ($previous_balance) = $self->previous; 
+
+    my ($previous_balance) = $self->previous;
     $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
     $previous_balance = sprintf('%.2f', $previous_balance);
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
     my @items = map {
@@ -2169,7 +1917,7 @@ sub print_csv {
             my $classnum = $cust_svc->part_svc->classnum;
             my $part_svc_class = FS::part_svc_class->by_key($classnum)
               if $classnum;
             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 ? 
+            $svc_class{$svcpart} = $part_svc_class ?
                                    $part_svc_class->classname :
                                    '';
           }
                                    $part_svc_class->classname :
                                    '';
           }
@@ -2208,7 +1956,7 @@ sub print_csv {
     return join('', $header, @details, "\n");
 
   } else { # default
     return join('', $header, @details, "\n");
 
   } else { # default
-  
+
     $csv->combine(
       'cust_bill',
       $self->invnum,
     $csv->combine(
       'cust_bill',
       $self->invnum,
@@ -2227,15 +1975,27 @@ sub print_csv {
   if ( lc($opt{'format'}) eq 'billco' ) {
 
     my $lineseq = 0;
   if ( lc($opt{'format'}) eq 'billco' ) {
 
     my $lineseq = 0;
-    foreach my $item ( $self->_items_pkg ) {
+    my %items_opt = ( format => 'template',
+                      escape_function => sub { shift } );
+    # I don't know what characters billco actually tolerates in spool entries.
+    # Text::CSV will take care of delimiters, though.
+
+    my @items = ( $self->_items_pkg(%items_opt),
+                  $self->_items_fee(%items_opt) );
+    foreach my $item (@items) {
+
+      my $description = $item->{'description'};
+      if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
+        $description .= ': ' . $item->{ext_description}[0];
+      }
 
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
 
       $csv->combine(
         '',                     #  1 | N/A-Leave Empty            CHAR   2
         '',                     #  2 | N/A-Leave Empty            CHAR  15
-        $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
+        $tracctnum,             #  3 | Account Number             CHAR  15
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
         $self->invnum,          #  4 | Invoice Number             CHAR  15
         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
-        $item->{'description'}, #  6 | Transaction Detail         CHAR 100
+        $description,           #  6 | Transaction Detail         CHAR 100
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
@@ -2256,7 +2016,7 @@ sub print_csv {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
 
       my($pkg, $setup, $recur, $sdate, $edate);
       if ( $cust_bill_pkg->pkgnum ) {
-      
+
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
         ($pkg, $setup, $recur, $sdate, $edate) = (
           $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
@@ -2265,21 +2025,21 @@ sub print_csv {
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
           ( $cust_bill_pkg->recur != 0
             ? sprintf("%.2f", $cust_bill_pkg->recur )
             : '' ),
-          ( $cust_bill_pkg->sdate 
+          ( $cust_bill_pkg->sdate
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
-          ($cust_bill_pkg->edate 
-            ?time2str("%x", $cust_bill_pkg->edate)
+          ($cust_bill_pkg->edate
+            ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
             : '' ),
         );
-  
+
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
       } else { #pkgnum tax
         next unless $cust_bill_pkg->setup != 0;
         $pkg = $cust_bill_pkg->desc;
         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
         ( $sdate, $edate ) = ( '', '' );
       }
-  
+
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
       $csv->combine(
         'cust_bill_pkg',
         $self->invnum,
@@ -2297,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 {
 sub comp {
-  my $self = shift;
-  my $cust_pay = new FS::cust_pay ( {
-    'invnum'   => $self->invnum,
-    'paid'     => $self->owed,
-    '_date'    => '',
-    'payby'    => 'COMP',
-    'payinfo'  => $self->cust_main->payinfo,
-    'paybatch' => '',
-  } );
-  $cust_pay->insert;
+  croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
 }
 
 =item realtime_card
 }
 
 =item realtime_card
@@ -2414,7 +2158,7 @@ sub batch_card {
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
   my $cust_main = $self->cust_main;
 
   $options{invnum} = $self->invnum;
-  
+
   $cust_main->batch_card(%options);
 }
 
   $cust_main->batch_card(%options);
 }
 
@@ -2438,7 +2182,7 @@ PNG file name is returned. Otherwise, the PNG image itself is returned.
 
 sub invoice_barcode {
     my ($self, $dir) = (shift,shift);
 
 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);
     my $gdbar = new GD::Barcode('Code39',$self->invnum);
        die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
     my $gd = $gdbar->plot(Height => 30);
@@ -2460,13 +2204,20 @@ sub invoice_barcode {
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
 =item invnum_date_pretty
 
 Returns a string with the invoice number and date, for example:
-"Invoice #54 (3/20/2008)"
+"Invoice #54 (3/20/2008)".
+
+Intended for back-end context, with regard to translation and date formatting.
 
 =cut
 
 
 =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;
 sub invnum_date_pretty {
   my $self = shift;
-  $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
+  #$self->mt('Invoice #').
+  'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
+    $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
 }
 
 #sub _items_extra_usage_sections {
 }
 
 #sub _items_extra_usage_sections {
@@ -2513,7 +2264,7 @@ sub _items_extra_usage_sections {
   my %classnums = ();
   my %lines = ();
 
   my %classnums = ();
   my %lines = ();
 
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
@@ -2526,13 +2277,13 @@ sub _items_extra_usage_sections {
       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
         my $amount = $detail->amount;
         next unless $amount && $amount > 0;
       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
         my $amount = $detail->amount;
         next unless $amount && $amount > 0;
+
         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
         $sections{$section}{amount} += $amount;  #subtotal
         $sections{$section}{calls}++;
         $sections{$section}{duration} += $detail->duration;
 
         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
         $sections{$section}{amount} += $amount;  #subtotal
         $sections{$section}{calls}++;
         $sections{$section}{duration} += $detail->duration;
 
-        my $desc = $detail->regionname; 
+        my $desc = $detail->regionname;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
         my $description = $desc;
         $description = substr($desc, 0, $maxlength). '...'
           if $format eq 'latex' && length($desc) > $maxlength;
@@ -2574,7 +2325,7 @@ sub _items_extra_usage_sections {
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
                               qw( description_generator header_generator total_generator total_line_generator )
                             )
                           : ()
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -2621,31 +2372,32 @@ sub _did_summary {
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            my $phone_deleted;
            $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
            my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
            my $phone_deleted;
            $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
-           
+
 # DID either activated or ported in; cannot be both for same DID simultaneously
            if ($inserted >= $start && $inserted <= $end && $phone_inserted
 # DID either activated or ported in; cannot be both for same DID simultaneously
            if ($inserted >= $start && $inserted <= $end && $phone_inserted
-               && (!$phone_inserted->lnp_status 
+               && (!$phone_inserted->lnp_status
                    || $phone_inserted->lnp_status eq ''
                    || $phone_inserted->lnp_status eq 'native')) {
                $num_activated++;
            }
            else { # this one not so clean, should probably move to (h_)svc_phone
                    || $phone_inserted->lnp_status eq ''
                    || $phone_inserted->lnp_status eq 'native')) {
                $num_activated++;
            }
            else { # this one not so clean, should probably move to (h_)svc_phone
+                 local($FS::Record::qsearch_qualify_columns) = 0;
                 my $phone_portedin = qsearchs( 'h_svc_phone',
                 my $phone_portedin = qsearchs( 'h_svc_phone',
-                     { 'svcnum' => $h_cust_svc->svcnum, 
-                       'lnp_status' => 'portedin' },  
-                     FS::h_svc_phone->sql_h_searchs($end),  
+                     { 'svcnum' => $h_cust_svc->svcnum,
+                       'lnp_status' => 'portedin' },
+                     FS::h_svc_phone->sql_h_searchs($end),
                    );
                 $num_portedin++ if $phone_portedin;
            }
 
 # DID either deactivated or ported out;        cannot be both for same DID simultaneously
            if($deleted >= $start && $deleted <= $end && $phone_deleted
                    );
                 $num_portedin++ if $phone_portedin;
            }
 
 # DID either deactivated or ported out;        cannot be both for same DID simultaneously
            if($deleted >= $start && $deleted <= $end && $phone_deleted
-               && (!$phone_deleted->lnp_status 
+               && (!$phone_deleted->lnp_status
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
                    || $phone_deleted->lnp_status ne 'portingout')) {
                $num_deactivated++;
-           } 
-           elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
-               && $phone_deleted->lnp_status 
+           }
+           elsif($deleted >= $start && $deleted <= $end && $phone_deleted
+               && $phone_deleted->lnp_status
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
                && $phone_deleted->lnp_status eq 'portingout') {
                $num_portedout++;
            }
@@ -2698,8 +2450,8 @@ sub _items_accountcode_cdr {
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
             $section->{'header'} = $detail->formatted('format' => $format)
-                if($detail->detail eq $section->{'header'}); 
-      
+                if($detail->detail eq $section->{'header'});
+
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
             my $accountcode = $detail->accountcode;
             next unless $accountcode;
 
@@ -2753,7 +2505,7 @@ sub _items_svc_phone_sections {
   my %classnums = ();
   my %lines = ();
 
   my %classnums = ();
   my %lines = ();
 
-  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
+  my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
 
   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
@@ -2782,7 +2534,7 @@ sub _items_svc_phone_sections {
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
       $sections{$phonenum}{calls}++;
       $sections{$phonenum}{duration} += $detail->duration;
 
-      my $desc = $detail->regionname; 
+      my $desc = $detail->regionname;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
       my $description = $desc;
       $description = substr($desc, 0, $maxlength). '...'
         if $format eq 'latex' && length($desc) > $maxlength;
@@ -2871,7 +2623,7 @@ sub _items_svc_phone_sections {
                                 total_line_generator
                               )
                           )
                                 total_line_generator
                               )
                           )
-                        ), 
+                        ),
                       };
   }
 
                       };
   }
 
@@ -2890,8 +2642,8 @@ sub _items_svc_phone_sections {
       push @lines, $l;
     }
   }
       push @lines, $l;
     }
   }
-  
-  if($conf->exists('phone_usage_class_summary')) { 
+
+  if($conf->exists('phone_usage_class_summary')) {
       # this only works with Latex
       my @newlines;
       my @newsections;
       # this only works with Latex
       my @newlines;
       my @newsections;
@@ -2917,7 +2669,7 @@ sub _items_svc_phone_sections {
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
            };
            $calls_detail{'description'} = 'Calls Detail: '
                                                    . $section->{'phonenum'};
-           push @newsections, \%calls_detail;  
+           push @newsections, \%calls_detail;
        }
       }
 
        }
       }
 
@@ -2926,7 +2678,7 @@ sub _items_svc_phone_sections {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
       foreach my $newsection ( @newsections ) {
        if($newsection->{'post_total'}) { # this means Calls Summary
            foreach my $section ( @sections ) {
-               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
+               next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
                                && !$section->{'post_total'});
                my $newdesc = $section->{'description'};
                my $tn = $section->{'phonenum'};
@@ -2974,114 +2726,843 @@ sub _items_svc_phone_sections {
 
 }
 
 
 }
 
-sub _items_previous {
+=item _items_usage_class_summary OPTIONS
+
+Returns a list of detail items summarizing the usage charges on this
+invoice.  Each one will have 'amount', 'description' (the usage charge name),
+and 'usage_classnum'.
+
+OPTIONS can include 'escape' (a function to escape the descriptions).
+
+=cut
+
+sub _items_usage_class_summary {
   my $self = shift;
   my $self = shift;
-  my $conf = $self->conf;
-  my $cust_main = $self->cust_main;
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-  my @b = ();
-  foreach ( @pr_cust_bill ) {
-    my $date = $conf->exists('invoice_show_prior_due_date')
-               ? 'due '. $_->due_date2str($date_format)
-               : time2str($date_format, $_->_date);
-    push @b, {
-      'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
-      #'pkgpart'     => 'N/A',
-      'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%.2f", $_->owed),
+  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,
     };
   }
     };
   }
-  @b;
+  $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
+  return @l;
+}
+
+=item _items_previous()
+
+  Returns an array of hashrefs, each hashref representing a line-item on
+  the current bill for previous unpaid invoices.
+
+  keys for each previous_item:
+  - amount (see notes)
+  - pkgnum
+  - description
+  - invnum
+  - _date
+
+  Payments and credits shown on this invoice may vary based on configuraiton.
+
+  when conf flag previous_balance-payments_since is set:
+    This method works backwards to rebuild the invoice as a snapshot in time.
+    The invoice displayed will have the balances owed, and payments made,
+    reflecting the state of the account at the time of invoice generation.
+
+=cut
+
+sub _items_previous {
+
+  my $self = shift;
+
+  # simple memoize
+  if ($self->get('_items_previous')) {
+    return sort { $a->{_date} <=> $b->{_date} }
+         values %{ $self->get('_items_previous') };
+  }
 
 
-  #{
-  #    '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 ],
+  # Gets the customer's current balance and outstanding invoices.
+  my ($prev_balance, @open_invoices) = $self->previous;
+
+  my %invoices = map {
+    $_->invnum => $self->__items_previous_map_invoice($_)
+  } @open_invoices;
+
+  # Which credits and payments displayed on the bill will vary based on
+  # conf flag previous_balance-payments_since.
+  my @credits  = $self->_items_credits();
+  my @payments = $self->_items_payments();
+
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    # For each credit or payment, determine which invoices it was applied to.
+    # Manipulate data displayed so the invoice displayed appears as a
+    # snapshot in time... with previous balances and balance owed displayed
+    # as they were at the time of invoice creation.
+
+    my @credits_postbill = $self->_items_credits_postbill();
+    my @payments_postbill = $self->_items_payments_postbill();
+
+    my %pmnt_dupechk;
+    my %cred_dupechk;
+
+    # Each section below follows this pattern on a payment/credit
+    #
+    # - Dupe check, avoid adjusting for the same item twice
+    # - If invoice being adjusted for isn't in our list, add it
+    # - Adjust the invoice balance to refelct balnace without the
+    #   credit or payment applied
+    #
+
+    # Working with payments displayed on this bill
+    for my $pmt_hash (@payments) {
+      my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
+      for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
+        next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
+        $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
+
+        my $invnum = $cust_bill_pay->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
+      }
+    }
+
+    # Working with credits displayed on this bill
+    for my $cred_hash (@credits) {
+      my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
+      for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
+        next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
+        $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
+
+        my $invnum = $cust_credit_bill->invnum;
+
+        $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+          unless exists $invoices{$invnum};
+
+        $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
+      }
+    }
+
+    # Working with both credits and payments which are not displayed
+    # on this bill, but which have affected this bill's balances
+    for my $postbill (@payments_postbill, @credits_postbill) {
+
+      if ($postbill->{billpaynum}) {
+        next if exists $pmnt_dupechk{$postbill->{billpaynum}};
+        $pmnt_dupechk{$postbill->{billpaynum}} = 1;
+      } elsif ($postbill->{creditbillnum}) {
+        next if exists $cred_dupechk{$postbill->{creditbillnum}};
+        $cred_dupechk{$postbill->{creditbillnum}} = 1;
+      } else {
+        die "Missing creditbillnum or billpaynum";
+      }
+
+      my $invnum = $postbill->{invnum};
+
+      $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
+        unless exists $invoices{$invnum};
+
+      $invoices{$invnum}->{amount} += $postbill->{amount};
+    }
+
+    # Make sure current invoice doesn't appear in previous items
+    delete $invoices{$self->invnum}
+      if exists $invoices{$self->invnum};
+
+  }
+
+  # Make sure amount is formatted as a dollar string
+  # (Formatting should happen on the template side, but is not?)
+  $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
+    for keys %invoices;
+
+  $self->set('_items_previous', \%invoices);
+  return sort { $a->{_date} <=> $b->{_date} } values %invoices;
 
 
-  #};
 }
 
 }
 
+=item _items_previous_total
+
+  Return sum of amounts from all items returned by _items_previous
+  Results will vary based on invoicing conf flags
+
+=cut
+
+sub _items_previous_total {
+  my $self = shift;
+  my $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 _items_credits()
+
+  Return array of hashrefs containing credits to be shown as line-items
+  when rendering this bill.
+
+  keys for each credit item:
+  - crednum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
+
+  This method has three ways it selects which credits to display on
+  this bill:
+
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+     Returns credits that have been applied to this bill only
+
+  2) Case:
+       Conf flag set for 'previous_balance-payments_since'
+
+     List all credits that have been recorded during the time period
+     between the timestamps of the last invoice and this invoice
+
+  3) Case:
+        Conf flag set for 'previous_balance-payments_since'
+        $opt{'template'} eq 'statement'
+
+    List all payments that have been recorded between the timestamps
+    of the previous invoice and the following invoice.
+
+     This is used to give the customer a receipt for a payment
+     in the form of their last bill with the payment amended.
+
+     I am concerned with this implementation, but leaving in place as is
+     If this option is selected, while viewing an older bill, the old bill
+     will show ALL future credits for future bills, but no charges for
+     future bills.  Somebody could be misled into believing they have a
+     large account credit when they don't.  Also, interrupts the chain of
+     invoices as an account history... the customer could have two invoices
+     in their fileing cabinet, for two different dates, both with a line item
+     for the same duplicate credit.  The accounting is technically accurate,
+     but somebody could easily become confused and think two credits were
+     made, when really those two line items on two different bills represent
+     only a single credit
+
+=cut
+
 sub _items_credits {
 sub _items_credits {
-  my( $self, %opt ) = @_;
-  my $trim_len = $opt{'trim_len'} || 60;
-
-  my @b;
-  #credits
-  my @objects;
-  if ( $self->conf->exists('previous_balance-payments_since') ) {
-    my $date = 0;
-    $date = $self->previous_bill->_date if $self->previous_bill;
-    @objects = qsearch('cust_credit', {
-        'custnum' => $self->custnum,
-        '_date'   => {op => '>=', value => $date},
+
+  my $self= shift;
+
+  # Simple memoize
+  return @{$self->get('_items_credits')} if $self->get('_items_credits');
+
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+  my $trim_len = $opt{template} || $self->get('trim_len') || 40;
+
+  my @return;
+  my @cust_credit_objs;
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+      # Case 3 (see above)
+      # Return credits timestamped between the previous and following bills
+
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
+
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
+
+      my %query = (
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_credit_objs = qsearch(\%query);
+
+    } else {
+      # Case 2 (see above)
+      # Return credits timestamps between this and the previous bills
+
+      my $date_start = 0;
+      my $date_end = $self->_date;
+
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
+
+      @cust_credit_objs = qsearch({
+        table => 'cust_credit',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
       });
       });
-      # hard to do this in the qsearch...
-    @objects = grep { $_->_date < $self->_date } @objects;
+    }
+
   } else {
   } else {
-    @objects = $self->cust_credited;
+    # Case 1 (see above)
+    # Return only credits that have been applied to this bill
+
+    @cust_credit_objs = $self->cust_credited;
+
   }
 
   }
 
-  foreach my $obj ( @objects ) {
+  # Translate objects into hashrefs
+  foreach my $obj ( @cust_credit_objs ) {
     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
+    my %r_obj = (
+      amount       => sprintf('%.2f',$cust_credit->amount),
+      crednum      => $cust_credit->crednum,
+      _date        => $cust_credit->_date,
+      creditreason => $cust_credit->reason,
+    );
 
     my $reason = substr($cust_credit->reason, 0, $trim_len);
     $reason .= '...' if length($reason) < length($cust_credit->reason);
 
     my $reason = substr($cust_credit->reason, 0, $trim_len);
     $reason .= '...' if length($reason) < length($cust_credit->reason);
-    $reason = " ($reason) " if $reason;
-
-    push @b, {
-      #'description' => 'Credit ref\#'. $_->crednum.
-      #                 " (". time2str("%x",$_->cust_credit->_date) .")".
-      #                 $reason,
-      'description' => $self->mt('Credit applied').' '.
-                       time2str($date_format,$obj->_date). $reason,
-      'amount'      => sprintf("%.2f",$obj->amount),
-    };
+    $reason = "($reason)" if $reason;
+
+    $r_obj{description} = join(' ',
+      $self->mt('Credit applied'),
+      $self->time2str_local('short', $cust_credit->_date),
+      $reason,
+    );
+
+    push @return, \%r_obj;
   }
   }
+  $self->set('_items_credits',\@return);
+  @return;
+}
 
 
-  @b;
+=item _items_credits_total
 
 
+  Return the total of al items from _items_credits
+  Will vary based on invoice display conf flag
+
+=cut
+
+sub _items_credits_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_credits();
+  return $tot;
 }
 
 }
 
+
+
+=item _items_credits_postbill()
+
+  Returns an array of hashrefs for credits where
+  - Credit issued after this invoice
+  - Credit applied to an invoice before this invoice
+
+  Returned hashrefs are of the format returned by _items_credits()
+
+=cut
+
+sub _items_credits_postbill {
+  my $self = shift;
+
+  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},
+#    },
+  });
+
+  return map {{
+    _date         => $_->_date,
+    invnum        => $_->invnum,
+    amount        => $_->amount,
+    creditbillnum => $_->creditbillnum,
+  }} @cust_credit_bill;
+}
+
+=item _items_payments_postbill()
+
+  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
+
+sub _items_payments_postbill {
+  my $self = shift;
+
+  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;
+}
+
+=item _items_payments()
+
+  Return array of hashrefs containing payments to be shown as line-items
+  when rendering this bill.
+
+  keys for each payment item:
+  - paynum: id of payment
+  - amount: payment amount
+  - description: line item to be displayed on the bill
+
+  This method has three ways it selects which payments to display on
+  this bill:
+
+  1) Default Case: No Conf flag for 'previous_balance-payments_since'
+
+     Returns payments that have been applied to this bill only
+
+  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_payments {
+
   my $self = shift;
 
   my $self = shift;
 
-  my @b;
-  my $detailed = $self->conf->exists('invoice_payment_details');
-  my @objects;
-  if ( $self->conf->exists('previous_balance-payments_since') ) {
-    my $date = 0;
-    $date = $self->previous_bill->_date if $self->previous_bill;
-    @objects = qsearch('cust_pay', {
-        'custnum' => $self->custnum,
-        '_date'   => {op => '>=', value => $date},
+  # Simple memoize
+  return @{$self->get('_items_payments')} if $self->get('_items_payments');
+
+  my %opt = @_;
+  my $template = $opt{template} || $self->get('_template');
+
+  my @return;
+  my @cust_pay_objs;
+
+  my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
+
+  if ($self->conf->exists('previous_balance-payments_since')) {
+    if ($template eq 'statement') {
+      # Case 3 (see above)
+      # Return payments timestamped between the previous and following bills
+
+      my $previous_bill  = $self->previous_bill;
+      my $following_bill = $self->following_bill;
+
+      my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
+      my $date_end   = ref $following_bill ? $following_bill->_date : undef;
+
+      my %query = (
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => { op => '>=', value => $date_start },
+        },
+      );
+      $query{extra_sql} = " AND _date <= $date_end " if $date_end;
+
+      @cust_pay_objs = qsearch(\%query);
+
+    } else {
+      # Case 2 (see above)
+      # Return payments timestamped between this and the previous bill
+
+      my $date_start = 0;
+      my $date_end = $self->_date;
+
+      my $previous_bill = $self->previous_bill;
+      if (ref $previous_bill) {
+        $date_start = $previous_bill->_date;
+      }
+
+      @cust_pay_objs = qsearch({
+        table => 'cust_pay',
+        hashref => {
+          custnum => $self->custnum,
+          _date => {op => '>=', value => $date_start},
+        },
+        extra_sql => " AND _date <= $date_end ",
       });
       });
-    @objects = grep { $_->_date < $self->_date } @objects;
+    }
+
   } else {
   } else {
-    @objects = $self->cust_bill_pay;
+    # Case 1 (see above)
+    # Return payments applied only to this bill
+
+    @cust_pay_objs = $self->cust_bill_pay;
+
   }
 
   }
 
-  foreach my $obj (@objects) {
+  $self->set(
+    '_items_payments',
+    [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
+  );
+  return @{ $self->get('_items_payments') };
+}
+
+=item _items_payments_total
+
+  Return a total of all records returned by _items_payments
+  Results vary based on invoicing conf flags
+
+=cut
+
+sub _items_payments_total {
+  my $self = shift;
+  my $tot = 0;
+  $tot += $_->{amount} for $self->_items_payments();
+  return $tot;
+}
+
+sub __items_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 $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
-    my $desc = $self->mt('Payment received').' '.
-               time2str($date_format, $cust_pay->_date );
-    $desc .= $self->mt(' via ') .
-             $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
-      if $detailed;
-
-    push @b, {
-      'description' => $desc,
-      'amount'      => sprintf("%.2f", $obj->amount )
+
+    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
+
+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();
+
+  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 $total_charges;
+  my $total_descr;
+
+  if ( $previous_balance && $c_multi_line_total ) {
+    # previous balance, new charges on separate lines
+
+    push @line_items, {
+      total_amount => sprintf('%.2f',$previous_balance),
+      total_item   => $self->mt(
+        $conf->config('previous_balance-text') || 'Previous Balance'
+      ),
     };
     };
+
+    $total_charges = $invoice_charges;
+    $total_descr   = $self->mt(
+      $conf->config('previous_balance-text-total_new_charges')
+      || 'Total New Charges'
+    );
+
+  } else {
+    # previous balance and new charges combined into a single total line
+    $total_charges = $invoice_charges + $previous_balance;
+    $total_descr = $self->mt('Total Charges');
+  }
+
+  if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
+    # then the due date should be shown with Total New Charges,
+    # and should NOT be shown with the Balance Due message.
+
+    if ( $self->due_date ) {
+      $total_descr .= $self->invoice_pay_by_msg;
+    } elsif ( $self->terms ) {
+      $total_descr = join(' ',
+        $total_descr,
+        '-',
+        $self->mt($self->terms)
+      );
+    }
   }
 
   }
 
-  @b;
+  push @line_items, {
+    total_amount => sprintf('%.2f', $total_charges),
+    total_item   => $total_descr,
+  };
+
+  return @line_items;
+}
+
+=item _items_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 ... ]
 }
 
 =item call_details [ OPTION => VALUE ... ]
@@ -3102,7 +3583,7 @@ sub call_details {
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
       my $row = shift;
 
       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
-      
+
     };
   }
 
     };
   }
 
@@ -3116,6 +3597,18 @@ sub call_details {
   ( $header, grep { $_ ne $header } @details );
 }
 
   ( $header, grep { $_ ne $header } @details );
 }
 
+=item cust_pay_batch
+
+Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
+will be removed.
+
+=cut
+
+sub cust_pay_batch {
+  carp "FS::cust_bill->cust_pay_batch is deprecated";
+  my $self = shift;
+  qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
+}
 
 =back
 
 
 =back
 
@@ -3163,14 +3656,12 @@ sub process_respool {
   process_re_X('spool', @_);
 }
 
   process_re_X('spool', @_);
 }
 
-use Storable qw(thaw);
 use Data::Dumper;
 use Data::Dumper;
-use MIME::Base64;
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
   warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
   warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   re_X(
   warn Dumper($param) if $DEBUG;
 
   re_X(
@@ -3181,6 +3672,9 @@ sub process_re_X {
 
 }
 
 
 }
 
+# this is called from search/cust_bill.html and given all its search
+# parameters, so it needs to perform the same search.
+
 sub re_X {
   # spool_invoice ftp_invoice fax_invoice print_invoice
   my($method, $job, %param ) = @_;
 sub re_X {
   # spool_invoice ftp_invoice fax_invoice print_invoice
   my($method, $job, %param ) = @_;
@@ -3190,22 +3684,15 @@ sub re_X {
   }
 
   #some false laziness w/search/cust_bill.html
   }
 
   #some false laziness w/search/cust_bill.html
-  my $distinct = '';
-  my $orderby = 'ORDER BY cust_bill._date';
-
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
-
-  my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
-     
-  my @cust_bill = qsearch( {
-    #'select'    => "cust_bill.*",
-    'table'     => 'cust_bill',
-    'addl_from' => $addl_from,
-    'hashref'   => {},
-    'extra_sql' => $extra_sql,
-    'order_by'  => $orderby,
-    'debug' => 1,
-  } );
+  $param{'order_by'} = 'cust_bill._date';
+
+  my $query = FS::cust_bill->search(\%param);
+  delete $query->{'count_query'};
+  delete $query->{'count_addl'};
+
+  $query->{debug} = 1; # was in here before, is obviously useful
+
+  my @cust_bill = qsearch( $query );
 
   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
 
 
   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
 
@@ -3231,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
 =back
 
 =head1 CLASS METHODS
@@ -3245,8 +3740,8 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
 
 sub owed_sql {
   my ($class, $start, $end) = @_;
-  'charged - '. 
-    $class->paid_sql($start, $end). ' - '. 
+  'charged - '.
+    $class->paid_sql($start, $end). ' - '.
     $class->credited_sql($start, $end);
 }
 
     $class->credited_sql($start, $end);
 }
 
@@ -3301,6 +3796,13 @@ Currently only supported on PostgreSQL.
 =cut
 
 sub due_date_sql {
 =cut
 
 sub due_date_sql {
+  die "don't use: doesn't account for agent-specific invoice_default_terms";
+
+  #we're passed a $conf but not a specific customer (that's in the query), so
+  # to make this work we'd need an agentnum-aware "condition_sql_conf" like
+  # "condition_sql_option" that retreives a conf value with SQL in an agent-
+  # aware fashion
+
   my $conf = new FS::Conf;
 'COALESCE(
   SUBSTRING(
   my $conf = new FS::Conf;
 'COALESCE(
   SUBSTRING(
@@ -3313,195 +3815,6 @@ sub due_date_sql {
 ) * 86400 + cust_bill._date'
 }
 
 ) * 86400 + cust_bill._date'
 }
 
-=item search_sql_where HASHREF
-
-Class method which returns an SQL WHERE fragment to search for parameters
-specified in HASHREF.  Valid parameters are
-
-=over 4
-
-=item _date
-
-List reference of start date, end date, as UNIX timestamps.
-
-=item invnum_min
-
-=item invnum_max
-
-=item agentnum
-
-=item charged
-
-List reference of charged limits (exclusive).
-
-=item owed
-
-List reference of charged limits (exclusive).
-
-=item open
-
-flag, return open invoices only
-
-=item net
-
-flag, return net invoices only
-
-=item days
-
-=item newest_percust
-
-=back
-
-Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
-
-=cut
-
-sub search_sql_where {
-  my($class, $param) = @_;
-  if ( $DEBUG ) {
-    warn "$me search_sql_where called with params: \n".
-         join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
-  }
-
-  my @search = ();
-
-  #agentnum
-  if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_main.agentnum = $1";
-  }
-
-  #refnum
-  if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_main.refnum = $1";
-  }
-
-  #custnum
-  if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.custnum = $1";
-  }
-
-  #customer classnum (false laziness w/ cust_main/Search.pm)
-  if ( $param->{'cust_classnum'} ) {
-
-    my @classnum = ref( $param->{'cust_classnum'} )
-                     ? @{ $param->{'cust_classnum'} }
-                     :  ( $param->{'cust_classnum'} );
-
-    @classnum = grep /^(\d*)$/, @classnum;
-
-    if ( @classnum ) {
-      push @search, '( '. join(' OR ', map {
-                                             $_ ? "cust_main.classnum = $_"
-                                                : "cust_main.classnum IS NULL"
-                                           }
-                                           @classnum
-                              ).
-                    ' )';
-    }
-
-  }
-
-  #_date
-  if ( $param->{_date} ) {
-    my($beginning, $ending) = @{$param->{_date}};
-
-    push @search, "cust_bill._date >= $beginning",
-                  "cust_bill._date <  $ending";
-  }
-
-  #invnum
-  if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.invnum >= $1";
-  }
-  if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
-    push @search, "cust_bill.invnum <= $1";
-  }
-
-  #charged
-  if ( $param->{charged} ) {
-    my @charged = ref($param->{charged})
-                    ? @{ $param->{charged} }
-                    : ($param->{charged});
-
-    push @search, map { s/^charged/cust_bill.charged/; $_; }
-                      @charged;
-  }
-
-  my $owed_sql = FS::cust_bill->owed_sql;
-
-  #owed
-  if ( $param->{owed} ) {
-    my @owed = ref($param->{owed})
-                 ? @{ $param->{owed} }
-                 : ($param->{owed});
-    push @search, map { s/^owed/$owed_sql/; $_; }
-                      @owed;
-  }
-
-  #open/net flags
-  push @search, "0 != $owed_sql"
-    if $param->{'open'};
-  push @search, '0 != '. FS::cust_bill->net_sql
-    if $param->{'net'};
-
-  #days
-  push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
-    if $param->{'days'};
-
-  #newest_percust
-  if ( $param->{'newest_percust'} ) {
-
-    #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
-    #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
-
-    my @newest_where = map { my $x = $_;
-                             $x =~ s/\bcust_bill\./newest_cust_bill./g;
-                             $x;
-                           }
-                           grep ! /^cust_main./, @search;
-    my $newest_where = scalar(@newest_where)
-                         ? ' AND '. join(' AND ', @newest_where)
-                        : '';
-
-
-    push @search, "cust_bill._date = (
-      SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
-        WHERE newest_cust_bill.custnum = cust_bill.custnum
-          $newest_where
-    )";
-
-  }
-
-  #promised_date - also has an option to accept nulls
-  if ( $param->{promised_date} ) {
-    my($beginning, $ending, $null) = @{$param->{promised_date}};
-
-    push @search, "(( cust_bill.promised_date >= $beginning AND ".
-                    "cust_bill.promised_date <  $ending )" .
-                    ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
-  }
-
-  #agent virtualization
-  my $curuser = $FS::CurrentUser::CurrentUser;
-  if ( $curuser->username eq 'fs_queue'
-       && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
-    my $username = $1;
-    my $newuser = qsearchs('access_user', {
-      'username' => $username,
-      'disabled' => '',
-    } );
-    if ( $newuser ) {
-      $curuser = $newuser;
-    } else {
-      warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
-    }
-  }
-  push @search, $curuser->agentnums_sql;
-
-  join(' AND ', @search );
-
-}
-
 =back
 
 =head1 BUGS
 =back
 
 =head1 BUGS
@@ -3517,4 +3830,3 @@ documentation.
 =cut
 
 1;
 =cut
 
 1;
-