RT#38671: Do not include charges and credits from failed signup processing [v4 only...
[freeside.git] / FS / FS / cust_bill.pm
index 7cee5d7..3ee6d47 100644 (file)
@@ -1,9 +1,12 @@
 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 vars qw( $DEBUG $me );
              # but NOT $conf
+use Carp;
 use Fcntl qw(:flock); #for spool_csv
 use Cwd;
 use List::Util qw(min max sum);
@@ -13,7 +16,7 @@ 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::cust_statement;
 use FS::cust_bill_pkg;
@@ -24,11 +27,9 @@ use FS::cust_pay;
 use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::pay_batch;
-use FS::cust_bill_event;
 use FS::cust_event;
 use FS::part_pkg;
 use FS::cust_bill_pay;
-use FS::part_bill_event;
 use FS::payby;
 use FS::bill_batch;
 use FS::cust_bill_batch;
@@ -36,6 +37,8 @@ use FS::cust_bill_pay_pkg;
 use FS::cust_credit_bill_pkg;
 use FS::discount_plan;
 use FS::cust_bill_void;
+use FS::reason;
+use FS::reason_type;
 use FS::L10N;
 
 $DEBUG = 0;
@@ -97,23 +100,19 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =back
 
-Customer info at invoice generation time
+Deprecated fields
 
 =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
 
@@ -129,6 +128,8 @@ Specific use cases
 
 =item promised_date - customer promised payment date, for collection
 
+=item pending - invoice is still being generated, empty or 'Y'
+
 =back
 
 =head1 METHODS
@@ -144,6 +145,16 @@ Invoices are normally created by calling the bill method of a customer object
 =cut
 
 sub table { 'cust_bill'; }
+sub template_conf { 'invoice_'; }
+
+sub has_sections {
+  my $self = shift;
+  my $agentnum = $self->cust_main->agentnum;
+  my $tc = $self->template_conf;
+
+  $self->conf->exists($tc.'sections', $agentnum) ||
+  $self->conf->exists($tc.'sections_by_location', $agentnum);
+}
 
 # should be the ONLY occurrence of "Invoice" in invoice rendering code.
 # (except email_subject and invnum_date_pretty)
@@ -203,7 +214,7 @@ sub insert {
 
 }
 
-=item void
+=item void [ REASON ]
 
 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
@@ -215,6 +226,14 @@ sub void {
   my $self = shift;
   my $reason = scalar(@_) ? shift : '';
 
+  unless (ref($reason) || !$reason) {
+    $reason = FS::reason->new_or_existing(
+      'class'  => 'I',
+      'type'   => 'Invoice void',
+      'reason' => $reason
+    );
+  }
+
   local $SIG{HUP} = 'IGNORE';
   local $SIG{INT} = 'IGNORE';
   local $SIG{QUIT} = 'IGNORE';
@@ -229,7 +248,7 @@ sub void {
   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;
@@ -244,7 +263,7 @@ sub void {
     }
   }
 
-  $error = $self->delete;
+  $error = $self->_delete;
   if ( $error ) {
     $dbh->rollback if $oldAutoCommit;
     return $error;
@@ -256,20 +275,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
-
-sub delete {
+# removed docs entirely and renamed method to _delete to further indicate it is
+# internal-only and discourage use
+#
+# =item delete
+# 
+# DO NOT USE THIS METHOD.  Instead, apply a credit against the invoice, or use
+# the B<void> method.
+# 
+# This is only for internal use by V<void>, which is what you should be using.
+# 
+# DO NOT USE THIS METHOD.  Whatever reason you think you have is almost certainly
+# wrong.  Use B<void>, that's what it is for.  Really.  This means you.
+# 
+# =cut
+
+sub _delete {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
 
@@ -285,15 +306,14 @@ sub delete {
   my $dbh = dbh;
 
   foreach my $table (qw(
-    cust_bill_event
-    cust_event
     cust_credit_bill
-    cust_bill_pay
-    cust_pay_batch
     cust_bill_pay_batch
+    cust_bill_pay
     cust_bill_batch
     cust_bill_pkg
   )) {
+    #cust_event # problematic
+    #cust_pay_batch # unnecessary
 
     foreach my $linked ( $self->$table() ) {
       my $error = $linked->delete;
@@ -338,6 +358,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
+                                    || $old->pending eq 'Y'
                                     || $old->charged == 0
                                    || $new->{'Hash'}{'cc_surcharge_replace_hack'};
 
@@ -392,6 +413,7 @@ sub check {
     || $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;
 
@@ -447,16 +469,20 @@ followed by the previous outstanding invoices (as FS::cust_bill objects also).
 
 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
@@ -563,32 +589,6 @@ sub open_cust_bill_pkg {
   @open;
 }
 
-=item cust_bill_event
-
-Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub cust_bill_event {
-  my $self = shift;
-  qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
-}
-
-=item num_cust_bill_event
-
-Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
-
-=cut
-
-sub num_cust_bill_event {
-  my $self = shift;
-  my $sql =
-    "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
-  my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
-  $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
-  $sth->fetchrow_arrayref->[0];
-}
-
 =item cust_event
 
 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
@@ -627,6 +627,23 @@ sub num_cust_event {
 
 Returns the customer (see L<FS::cust_main>) for this invoice.
 
+=item suspend
+
+Suspends all unsuspended packages (see L<FS::cust_pkg>) for this invoice
+
+Returns a list: an empty list on success or a list of errors.
+
+=cut
+
+sub suspend {
+  my $self = shift;
+
+  grep { $_->suspend(@_) } 
+  grep {! $_->getfield('cancel') } 
+  $self->cust_pkg;
+
+}
+
 =item cust_suspend_if_balance_over AMOUNT
 
 Suspends the customer associated with this invoice if the total amount owed on
@@ -646,6 +663,37 @@ sub cust_suspend_if_balance_over {
   }
 }
 
+=item cancel
+
+Cancel the packages on this invoice. Largely similar to the cust_main version, but does not bother yet with banned payment options
+
+=cut
+
+sub cancel {
+  my( $self, %opt ) = @_;
+
+  warn "$me cancel called on cust_bill ". $self->invnum . " with options ".
+       join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
+    if $DEBUG;
+
+  return ( 'Access denied' )
+    unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
+
+  my @pkgs = $self->cust_pkg;
+
+  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;
+  }
+
+  grep { $_ }
+    map { $_->cancel(%opt) }
+      grep { ! $_->getfield('cancel') } 
+        @pkgs;
+}
+
 =item cust_bill_pay
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
@@ -852,6 +900,7 @@ sub hide {
 =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
@@ -878,7 +927,9 @@ sub apply_payments_and_credits {
 
   $self->select_for_update; #mutex
 
-  my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
+  my @payments = grep { $_->unapplied > 0 } 
+                   grep { !$_->no_auto_apply }
+                     $self->cust_main->cust_pay;
   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
 
   if ( $conf->exists('pkg-balances') ) {
@@ -974,301 +1025,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),
-    'custnum'   => $self->custnum,
-    'msgtype'   => 'invoice',
-  );
-
-  $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',
-      'Charset'     => 'UTF-8',
-      #'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
@@ -1281,7 +1037,7 @@ I<template>: a suffix for alternate invoices
 
 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
 
@@ -1304,7 +1060,7 @@ sub send {
 
   $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
@@ -1317,55 +1073,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;
-  return if $self->hide;
-  my $conf = $self->conf;
   my $opt = shift || {};
   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
-            $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') ) {
@@ -1376,19 +1099,32 @@ 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};
+
+  if ( $opt{mode} ) {
+    $self->set('mode', $opt{mode});
+  }
+
+  my %args = map {$_ => $opt{$_}} 
+             grep { $opt{$_} }
+              qw( from notice_name no_coupon template );
+
+  my $error = $self->email( \%args );
+  die $error if $error;
 
 }
 
@@ -1511,6 +1247,8 @@ sub fax_invoice {
 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
 isn't an open batch, one will be created.
 
+HASHREF may contain any options to be passed to C<print_pdf>.
+
 =cut
 
 sub batch_invoice {
@@ -2167,7 +1905,19 @@ sub print_csv {
   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
@@ -2175,7 +1925,7 @@ sub print_csv {
         $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
+        $description,           #  6 | Transaction Detail         CHAR 100
         $item->{'amount'},      #  7 | Amount                     NUM*   9
         '',                     #  8 | Line Format Control**      CHAR   2
         '',                     #  9 | Grouping Code              CHAR   2
@@ -2237,24 +1987,8 @@ sub print_csv {
 
 }
 
-=item comp
-
-Pays this invoice with a compliemntary payment.  If there is an error,
-returns the error, otherwise returns false.
-
-=cut
-
 sub comp {
-  my $self = shift;
-  my $cust_pay = new FS::cust_pay ( {
-    'invnum'   => $self->invnum,
-    'paid'     => $self->owed,
-    '_date'    => '',
-    'payby'    => 'COMP',
-    'payinfo'  => $self->cust_main->payinfo,
-    'paybatch' => '',
-  } );
-  $cust_pay->insert;
+  croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
 }
 
 =item realtime_card
@@ -2460,7 +2194,7 @@ sub _items_extra_usage_sections {
   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 ) {
@@ -2577,6 +2311,7 @@ sub _did_summary {
                $num_activated++;
            }
            else { # this one not so clean, should probably move to (h_)svc_phone
+                 local($FS::Record::qsearch_qualify_columns) = 0;
                 my $phone_portedin = qsearchs( 'h_svc_phone',
                      { 'svcnum' => $h_cust_svc->svcnum, 
                        'lnp_status' => 'portedin' },  
@@ -2700,7 +2435,7 @@ sub _items_svc_phone_sections {
   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 };
@@ -2936,10 +2671,12 @@ sub _items_usage_class_summary {
   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',
+      '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".
@@ -2950,17 +2687,21 @@ sub _items_usage_class_summary {
   my @l;
   my $section = {
     description   => &{$escape}($self->mt('Usage Summary')),
-    no_subtotal   => 1,
     usage_section => 1,
+    subtotal      => 0,
   };
   foreach my $class (@classes) {
+    $section->{subtotal} += $class->get('amount');
     push @l, {
       'description'     => &{$escape}($class->classname),
-      'amount'          => sprintf('%.2f', $class->amount),
+      'amount'          => $money_char.sprintf('%.2f', $class->get('amount')),
+      'quantity'        => $class->get('calls'),
+      'duration'        => $class->get('duration'),
       'usage_classnum'  => $class->classnum,
       'section'         => $section,
     };
   }
+  $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
   return @l;
 }
 
@@ -2999,7 +2740,7 @@ sub _items_previous {
 
 sub _items_credits {
   my( $self, %opt ) = @_;
-  my $trim_len = $opt{'trim_len'} || 60;
+  my $trim_len = $opt{'trim_len'} || 40;
 
   my @b;
   #credits
@@ -3096,6 +2837,76 @@ sub _items_payments {
 
 }
 
+sub _items_total {
+  my $self = shift;
+  my $conf = $self->conf;
+
+  my @items;
+  my ($pr_total) = $self->previous;
+  my ($previous_charges_desc, $new_charges_desc, $new_charges_amount);
+
+  if ( $conf->exists('previous_balance-exclude_from_total') ) {
+    # if enabled, specifically add a line for the previous balance total
+    $previous_charges_desc = $self->mt(
+      $conf->config('previous_balance-text') || 'Previous Balance'
+    );
+
+    # then return separate lines for previous balance and total new charges
+    if ( $pr_total ) {
+      push @items,
+        { total_item    => $previous_charges_desc,
+          total_amount  => sprintf('%.2f',$pr_total)
+        };
+    }
+  }
+
+  if (   $conf->exists('previous_balance-exclude_from_total')
+      or !$self->enable_previous ) {
+    # show new charges only
+
+    $new_charges_desc = $self->mt(
+      $conf->config('previous_balance-text-total_new_charges')
+       || 'Total New Charges'
+    );
+
+    $new_charges_amount = $self->charged;
+
+  } else {
+    # show new charges + previous invoice total
+
+    $new_charges_desc = $self->mt('Total Charges');
+    if ( $self->enable_previous ) {
+      $new_charges_amount = sprintf('%.2f', $self->charged + $pr_total);
+    } else {
+      $new_charges_amount = sprintf('%.2f', $self->charged);
+    }
+
+  }
+
+  if ( $conf->exists('invoice_show_prior_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 ) {
+      # localize the "Please pay by" message and the date itself
+      # (grammar issues with this, yeah)
+      $new_charges_desc .= ' - ' . $self->mt('Please pay by') . ' ' .
+                           $self->due_date2str('short');
+    } elsif ( $self->terms ) {
+      # phrases like "due on receipt" should be localized
+      $new_charges_desc .= ' - ' . $self->mt($self->terms);
+    }
+  }
+
+  push @items,
+    { total_item    => $new_charges_desc,
+      total_amount  => $new_charges_amount,
+    };
+
+  @items;
+}
+
+
+
 =item call_details [ OPTION => VALUE ... ]
 
 Returns an array of CSV strings representing the call details for this invoice
@@ -3128,6 +2939,18 @@ sub call_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
 
@@ -3175,14 +2998,12 @@ sub process_respool {
   process_re_X('spool', @_);
 }
 
-use Storable qw(thaw);
 use Data::Dumper;
-use MIME::Base64;
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
   warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
-  my $param = thaw(decode_base64(shift));
+  my $param = shift;
   warn Dumper($param) if $DEBUG;
 
   re_X(
@@ -3193,6 +3014,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 ) = @_;
@@ -3202,22 +3026,15 @@ sub re_X {
   }
 
   #some false laziness w/search/cust_bill.html
-  my $distinct = '';
-  my $orderby = 'ORDER BY cust_bill._date';
-
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_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';
 
@@ -3321,6 +3138,13 @@ Currently only supported on PostgreSQL.
 =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(
@@ -3333,219 +3157,6 @@ sub due_date_sql {
 ) * 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
-
-=item custnum
-
-Return only invoices belonging to that customer.
-
-=item cust_classnum
-
-Limit to that customer class (single value or arrayref).
-
-=item payby
-
-Limit to customers with that payment method (single value or arrayref).
-
-=item refnum
-
-Limit to customers with that advertising source.
-
-=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
-                              ).
-                    ' )';
-    }
-
-  }
-
-  #payby
-  if ( $param->{payby} ) {
-    my $payby = $param->{payby};
-    $payby = [ $payby ] unless ref $payby;
-    my $payby_in = join(',', map {dbh->quote($_)} @$payby);
-    push @search, "cust_main.payby IN($payby_in)" if length($payby_in);
-  }
-
-  #_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