Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / FS / FS / cust_bill.pm
index f85bca6..3c0e3e7 100644 (file)
@@ -2,7 +2,7 @@ package FS::cust_bill;
 use base qw( FS::Template_Mixin FS::cust_main_Mixin FS::Record );
 
 use strict;
-use vars qw( $DEBUG $me $date_format );
+use vars qw( $DEBUG $me );
              # but NOT $conf
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
@@ -15,7 +15,6 @@ use GD::Barcode;
 use FS::UID qw( datasrc );
 use FS::Misc qw( send_email send_fax do_print );
 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;
@@ -25,12 +24,10 @@ use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
-use FS::cust_pay_batch;
 use FS::cust_bill_event;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
-use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
 use FS::bill_batch;
@@ -47,7 +44,6 @@ $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
@@ -80,7 +76,7 @@ FS::cust_bill - Object methods for cust_bill records
   $tax_amount = $record->tax;
 
   @lines = $cust_bill->print_text;
-  @lines = $cust_bill->print_text $time;
+  @lines = $cust_bill->print_text('time' => $time);
 
 =head1 DESCRIPTION
 
@@ -110,9 +106,11 @@ Customer info at invoice generation time
 
 =over 4
 
-=item previous_balance
+=item billing_balance - the customer's balance at the time the invoice was 
+generated (not including charges on this invoice)
 
-=item billing_balance
+=item previous_balance - the billing_balance of this customer's previous 
+invoice plus the charges on that invoice
 
 =back
 
@@ -151,9 +149,15 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
-sub notice_name { 'Invoice'; }
 
-sub cust_linked { $_[0]->cust_main_custnum; } 
+# should be the ONLY occurrence of "Invoice" in invoice rendering code.
+# (except email_subject and invnum_date_pretty)
+sub notice_name {
+  my $self = shift;
+  $self->conf->config('notice_name') || 'Invoice'
+}
+
+sub cust_linked { $_[0]->cust_main_custnum || $_[0]->custnum } 
 sub cust_unlinked_msg {
   my $self = shift;
   "WARNING: can't find cust_main.custnum ". $self->custnum.
@@ -420,6 +424,25 @@ sub display_invnum {
   }
 }
 
+=item previous_bill
+
+Returns the customer's last invoice before this one.
+
+=cut
+
+sub previous_bill {
+  my $self = shift;
+  if ( !$self->get('previous_bill') ) {
+    $self->set('previous_bill', qsearchs({
+          'table'     => 'cust_bill',
+          'hashref'   => { 'custnum'  => $self->custnum,
+                           '_date'    => { op=>'<', value=>$self->_date } },
+          'order_by'  => 'ORDER BY _date DESC LIMIT 1',
+    }) );
+  }
+  $self->get('previous_bill');
+}
+
 =item previous
 
 Returns a list consisting of the total previous balance for this customer, 
@@ -465,7 +488,9 @@ sub cust_bill_pkg {
   qsearch(
     { 'table'    => 'cust_bill_pkg',
       '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?
     }
   );
 }
@@ -607,13 +632,6 @@ sub num_cust_event {
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
-=cut
-
-sub cust_main {
-  my $self = shift;
-  qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
-}
-
 =item cust_suspend_if_balance_over AMOUNT
 
 Suspends the customer associated with this invoice if the total amount owed on
@@ -674,16 +692,6 @@ sub cust_pay {
   #;
 }
 
-sub cust_pay_batch {
-  my $self = shift;
-  qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
-}
-
-sub cust_bill_pay_batch {
-  my $self = shift;
-  qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
-}
-
 =item cust_bill_pay
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
@@ -1022,7 +1030,7 @@ Options:
 
 sender address, required
 
-=item tempate
+=item template
 
 alternate template name, optional
 
@@ -1056,15 +1064,12 @@ sub generate_email {
 
   my %return = (
     'from'      => $args{'from'},
-    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+    'subject'   => ($args{'subject'} || $self->email_subject),
+    'custnum'   => $self->custnum,
+    'msgtype'   => 'invoice',
   );
 
-  my %opt = (
-    'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
-    'template'      => $args{'template'},
-    'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
-    'no_coupon'     => $args{'no_coupon'},
-  );
+  $args{'unsquelch_cdr'} = $conf->exists('voip-cdr_email');
 
   my $cust_main = $self->cust_main;
 
@@ -1106,7 +1111,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
-        $data = [ $self->print_text(\%opt) ];
+        $data = [ $self->print_text(\%args) ];
       }
 
     }
@@ -1163,10 +1168,10 @@ sub generate_email {
           'Filename'   => 'barcode.png',
           'Content-ID' => "<$barcode_content_id>",
         ;
-        $opt{'barcode_cid'} = $barcode_content_id;
+        $args{'barcode_cid'} = $barcode_content_id;
       }
 
-      $htmldata = $self->print_html({ 'cid'=>$content_id, %opt });
+      $htmldata = $self->print_html({ 'cid'=>$content_id, %args });
     }
 
     $alternative->attach(
@@ -1228,7 +1233,7 @@ sub generate_email {
 
       $related->add_part($image) if $image;
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
+      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%args);
 
       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
@@ -1260,7 +1265,7 @@ sub generate_email {
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
-        { $self->mimebuild_pdf(\%opt) }
+        { $self->mimebuild_pdf(\%args) }
       ];
     }
   
@@ -1280,7 +1285,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
-        $return{'body'} = [ $self->print_text(\%opt) ];
+        $return{'body'} = [ $self->print_text(\%args) ];
       }
 
     }
@@ -1309,105 +1314,48 @@ sub mimebuild_pdf {
   );
 }
 
-=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
+=item send HASHREF
 
 Sends this invoice to the destinations configured for this customer: sends
 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
-Options can be passed as a hashref (recommended) or as a list of up to 
-four values for templatename, agentnum, invoice_from and amount.
+Options can 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<template>: a suffix for alternate invoices
 
-I<agentnum>, if specified, means that this invoice will only be sent for customers
-of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
-single agent) or an arrayref of agentnums.
+I<agentnum>: obsolete, now does nothing.
 
-I<invoice_from>, if specified, overrides the default email invoice From: address.
+I<invoice_from> overrides the default email invoice From: address.
 
-I<amount>, if specified, only sends the invoice if the total amount owed on this
-invoice and all older invoices is greater than the specified amount.
+I<amount>: obsolete, does nothing
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> overrides "Invoice" as the name of the sent document 
+(templates from 10/2009 or newer required).
 
-I<lpr>, if specified, is passed to 
+I<lpr> overrides the system 'lpr' option as the command to print a document
+from standard input.
 
 =cut
 
-sub queueable_send {
-  my %opt = @_;
-
-  my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
-    or die "invalid invoice number: " . $opt{invnum};
-
-  my @args = ( $opt{template}, $opt{agentnum} );
-  push @args, $opt{invoice_from}
-    if exists($opt{invoice_from}) && $opt{invoice_from};
-
-  my $error = $self->send( @args );
-  die $error if $error;
-
-}
-
 sub send {
   my $self = shift;
+  my $opt = ref($_[0]) ? $_[0] : +{ @_ };
   my $conf = $self->conf;
 
-  my( $template, $invoice_from, $notice_name );
-  my $agentnums = '';
-  my $balance_over = 0;
-  my $lpr = '';
-
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    if ( $agentnums = $opt->{'agentnum'} ) {
-      $agentnums = [ $agentnums ] unless ref($agentnums);
-    }
-    $invoice_from = $opt->{'invoice_from'};
-    $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
-    $notice_name = $opt->{'notice_name'};
-    $lpr = $opt->{'lpr'}
-  } else {
-    $template = scalar(@_) ? shift : '';
-    if ( scalar(@_) && $_[0]  ) {
-      $agentnums = ref($_[0]) ? shift : [ shift ];
-    }
-    $invoice_from = shift if scalar(@_);
-    $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
-  }
-
   my $cust_main = $self->cust_main;
 
-  return 'N/A' unless ! $agentnums
-                   or grep { $_ == $cust_main->agentnum } @$agentnums;
-
-  return ''
-    unless $cust_main->total_owed_date($self->_date) > $balance_over;
-
-  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
-                    $conf->config('invoice_from', $cust_main->agentnum );
-
-  my %opt = (
-    'template'     => $template,
-    'invoice_from' => $invoice_from,
-    'notice_name'  => ( $notice_name || 'Invoice' ),
-  );
-
   my @invoicing_list = $cust_main->invoicing_list;
 
-  #$self->email_invoice(\%opt)
-  $self->email(\%opt)
+  $self->email($opt)
     if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list )
     && ! $self->invoice_noemail;
 
-  $opt{'lpr'} = $lpr;
-  #$self->print_invoice(\%opt)
-  $self->print(\%opt)
+  $self->print($opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
   #this has never been used post-$ORIGINAL_ISP afaik
-  $self->fax_invoice(\%opt)
+  $self->fax_invoice($opt)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
@@ -1416,16 +1364,17 @@ sub send {
 
 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
 
-Emails this invoice.
+Sends this invoice to the customer's email destination(s).
 
-Options can be passed as a hashref (recommended) or as a list of up to 
-two values for templatename and invoice_from.
+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<invoice_from>, if specified, overrides the default email invoice From: 
+address.
 
-I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+I<notice_name> is the name of the sent document.
 
 =cut
 
@@ -1435,38 +1384,30 @@ sub queueable_email {
   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
     or die "invalid invoice number: " . $opt{invnum};
 
-  my %args = ( 'template' => $opt{template} );
-  $args{$_} = $opt{$_}
-    foreach grep { exists($opt{$_}) && $opt{$_} }
-              qw( invoice_from notice_name no_coupon );
+  my %args = map {$_ => $opt{$_}} 
+             grep { $opt{$_} }
+              qw( invoice_from notice_name no_coupon template );
 
   my $error = $self->email( \%args );
   die $error if $error;
 
 }
 
-#sub email_invoice {
 sub email {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $invoice_from, $notice_name, $no_coupon );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $invoice_from = $opt->{'invoice_from'};
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-    $no_coupon = $opt->{'no_coupon'} || 0;
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $invoice_from = shift if scalar(@_);
-    $notice_name = 'Invoice';
-    $no_coupon = 0;
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::email called with positional parameters";
   }
 
-  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
-                    $conf->config('invoice_from', $self->cust_main->agentnum );
+  my $template = $opt->{template};
+  my $from = delete $opt->{invoice_from};
+
+  # 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 );
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
@@ -1476,20 +1417,19 @@ sub email {
       die 'No recipients for customer #'. $self->custnum;
     } else {
       #default: better to notify this person than silence
-      @invoicing_list = ($invoice_from);
+      @invoicing_list = ($from);
     }
   }
 
+  # this is where we set the Subject:
   my $subject = $self->email_subject($template);
 
   my $error = send_email(
     $self->generate_email(
-      'from'        => $invoice_from,
+      'from'        => $from,
       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
       'subject'     => $subject,
-      'template'    => $template,
-      'notice_name' => $notice_name,
-      'no_coupon'   => $no_coupon,
+      %$opt, # template, etc.
     )
   );
   die "can't email invoice: $error\n" if $error;
@@ -1516,12 +1456,12 @@ sub email_subject {
   eval qq("$subject");
 }
 
-=item lpr_data HASHREF | [ TEMPLATE ]
+=item lpr_data HASHREF
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-Options can be passed as a hashref (recommended) or as a single optional value
-for template.
+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.
 
@@ -1532,31 +1472,21 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 sub lpr_data {
   my $self = shift;
   my $conf = $self->conf;
-  my( $template, $notice_name );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    # nobody does this anyway
+    die "FS::cust_bill::lpr_data called with positional parameters";
   }
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
-  [ $self->$method( \%opt ) ];
+  [ $self->$method( $opt ) ];
 }
 
-=item print HASHREF | [ TEMPLATE ]
+=item print HASHREF
 
 Prints this invoice.
 
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1564,48 +1494,34 @@ I<notice_name>, if specified, overrides "Invoice" as the name of the sent docume
 
 =cut
 
-#sub print_invoice {
 sub print {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $notice_name, $lpr );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-    $lpr = $opt->{'lpr'}
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
-    $lpr = '';
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::print called with positional parameters";
   }
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
+  my $lpr = delete $opt->{lpr};
   if($conf->exists('invoice_print_pdf')) {
     # Add the invoice to the current batch.
-    $self->batch_invoice(\%opt);
+    $self->batch_invoice($opt);
   }
   else {
     do_print(
-      $self->lpr_data(\%opt),
+      $self->lpr_data($opt),
       'agentnum' => $self->cust_main->agentnum,
       'lpr'      => $lpr,
     );
   }
 }
 
-=item fax_invoice HASHREF | [ TEMPLATE ] 
+=item fax_invoice HASHREF
 
 Faxes this invoice.
 
-Options can be passed as a hashref (recommended) or as a single optional
-value for template.
+Options must be passed as a hashref.
 
 I<template>, if specified, is the name of a suffix for alternate invoices.
 
@@ -1617,15 +1533,9 @@ sub fax_invoice {
   my $self = shift;
   return if $self->hide;
   my $conf = $self->conf;
-
-  my( $template, $notice_name );
-  if ( ref($_[0]) ) {
-    my $opt = shift;
-    $template = $opt->{'template'} || '';
-    $notice_name = $opt->{'notice_name'} || 'Invoice';
-  } else {
-    $template = scalar(@_) ? shift : '';
-    $notice_name = 'Invoice';
+  my $opt = shift || {};
+  if ($opt and !ref($opt)) {
+    die "FS::cust_bill::fax_invoice called with positional parameters";
   }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
@@ -1634,12 +1544,7 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my %opt = (
-    'template'    => $template,
-    'notice_name' => $notice_name,
-  );
-
-  my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
+  my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
@@ -1728,29 +1633,6 @@ sub spool_invoice {
   );
 }
 
-=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
-
-Like B<send>, but only sends the invoice if it is the newest open invoice for
-this customer.
-
-=cut
-
-sub send_if_newest {
-  my $self = shift;
-
-  return ''
-    if scalar(
-               grep { $_->owed > 0 } 
-                    qsearch('cust_bill', {
-                      'custnum' => $self->custnum,
-                      #'_date'   => { op=>'>', value=>$self->_date },
-                      'invnum'  => { op=>'>', value=>$self->invnum },
-                    } )
-             );
-    
-  $self->send(@_);
-}
-
 =item send_csv OPTION => VALUE, ...
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
@@ -1778,6 +1660,7 @@ sub send_csv {
   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";
   
@@ -2071,25 +1954,33 @@ sub print_csv {
 
   my $time = $opt{'time'} || time;
 
+  my $tracctnum = ''; #leaking out from billco-specific sections :/
   if ( $format eq 'billco' ) {
 
+    my $account_num =
+      $self->conf->config('billco-account_num', $cust_main->agentnum);
+
+    $tracctnum = $account_num eq 'display_custnum'
+                   ? $cust_main->display_custnum
+                   : $opt{'tracctnum'};
+
     my $taxtotal = 0;
     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
 
-    my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
+    my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
 
     my( $previous_balance, @unused ) = $self->previous; #previous balance
 
     my $pmt_cr_applied = 0;
     $pmt_cr_applied += $_->{'amount'}
-      foreach ( $self->_items_payments, $self->_items_credits ) ;
+      foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
 
     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
 
     $csv->combine(
       '',                         #  1 | N/A-Leave Empty               CHAR   2
       '',                         #  2 | N/A-Leave Empty               CHAR  15
-      $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
+      $tracctnum,                 #  3 | Transaction Account No        CHAR  15
       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
@@ -2326,7 +2217,7 @@ sub print_csv {
       $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
         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
@@ -2363,7 +2254,7 @@ sub print_csv {
             ? time2str("%x", $cust_bill_pkg->sdate)
             : '' ),
           ($cust_bill_pkg->edate 
-            ?time2str("%x", $cust_bill_pkg->edate)
+            ? time2str("%x", $cust_bill_pkg->edate)
             : '' ),
         );
   
@@ -3076,8 +2967,8 @@ sub _items_previous {
   my @b = ();
   foreach ( @pr_cust_bill ) {
     my $date = $conf->exists('invoice_show_prior_due_date')
-               ? 'due '. $_->due_date2str($date_format)
-               : time2str($date_format, $_->_date);
+               ? 'due '. $_->due_date2str('short')
+               : $self->time2str_local('short', $_->_date);
     push @b, {
       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
       #'pkgpart'     => 'N/A',
@@ -3107,12 +2998,33 @@ sub _items_credits {
 
   my @b;
   #credits
-  foreach ( $self->cust_credited ) {
+  my @objects;
+  if ( $self->conf->exists('previous_balance-payments_since') ) {
+    if ( $opt{'template'} eq 'statement' ) {
+      # then the current bill is a "statement" (i.e. an invoice sent as
+      # a payment receipt)
+      # and in that case we want to see payments on or after THIS invoice
+      @objects = qsearch('cust_credit', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $self->_date},
+      });
+    } else {
+      my $date = 0;
+      $date = $self->previous_bill->_date if $self->previous_bill;
+      @objects = qsearch('cust_credit', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $date},
+      });
+    }
+  } else {
+    @objects = $self->cust_credited;
+  }
 
-    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+  foreach my $obj ( @objects ) {
+    my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
 
-    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, {
@@ -3120,8 +3032,8 @@ sub _items_credits {
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => $self->mt('Credit applied').' '.
-                       time2str($date_format,$_->cust_credit->_date). $reason,
-      'amount'      => sprintf("%.2f",$_->amount),
+                       $self->time2str_local('short', $obj->_date). $reason,
+      'amount'      => sprintf("%.2f",$obj->amount),
     };
   }
 
@@ -3131,23 +3043,48 @@ sub _items_credits {
 
 sub _items_payments {
   my $self = shift;
+  my %opt = @_;
 
   my @b;
-  #get & print payments
-  foreach ( $self->cust_bill_pay ) {
-
-    #something more elaborate if $_->amount ne ->cust_pay->paid ?
+  my $detailed = $self->conf->exists('invoice_payment_details');
+  my @objects;
+  if ( $self->conf->exists('previous_balance-payments_since') ) {
+    # then show payments dated on/after the previous bill...
+    if ( $opt{'template'} eq 'statement' ) {
+      # then the current bill is a "statement" (i.e. an invoice sent as
+      # a payment receipt)
+      # and in that case we want to see payments on or after THIS invoice
+      @objects = qsearch('cust_pay', {
+          'custnum' => $self->custnum,
+          '_date'   => {op => '>=', value => $self->_date},
+      });
+    } else {
+      # the normal case: payments on or after the previous invoice
+      my $date = 0;
+      $date = $self->previous_bill->_date if $self->previous_bill;
+      @objects = qsearch('cust_pay', {
+        'custnum' => $self->custnum,
+        '_date'   => {op => '>=', value => $date},
+      });
+      # and before the current bill...
+      @objects = grep { $_->_date < $self->_date } @objects;
+    }
+  } else {
+    @objects = $self->cust_bill_pay;
+  }
 
+  foreach my $obj (@objects) {
+    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)
-      if ( $self->conf->exists('invoice_payment_details') );
+               $self->time2str_local('short', $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", $_->amount )
+      'amount'      => sprintf("%.2f", $obj->amount )
     };
-
   }
 
   @b;
@@ -3450,13 +3387,25 @@ sub search_sql_where {
     push @search, "cust_bill.custnum = $1";
   }
 
-  #customer classnum
+  #customer classnum (false laziness w/ cust_main/Search.pm)
   if ( $param->{'cust_classnum'} ) {
-    my $classnums = $param->{'cust_classnum'};
-    $classnums = [ $classnums ] if !ref($classnums);
-    $classnums = [ grep /^\d+$/, @$classnums ];
-    push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
-      if @$classnums;
+
+    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