fix invoice sub-totals, RT#6489
[freeside.git] / FS / FS / cust_bill.pm
index 79f85c8..a0885f1 100644 (file)
@@ -96,6 +96,18 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =item charged - amount of this invoice
 
+=item invoice_terms - optional terms override for this specific invoice
+
+=back
+
+Customer info at invoice generation time
+
+=over 4
+
+=item previous_balance
+
+=item billing_balance
+
 =back
 
 Deprecated
@@ -160,7 +172,50 @@ Really, don't use it.
 sub delete {
   my $self = shift;
   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
-  $self->SUPER::delete(@_);
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  foreach my $table (qw(
+    cust_bill_event
+    cust_event
+    cust_credit_bill
+    cust_bill_pay
+    cust_bill_pay
+    cust_credit_bill
+    cust_pay_batch
+    cust_bill_pay_batch
+    cust_bill_pkg
+  )) {
+
+    foreach my $linked ( $self->$table() ) {
+      my $error = $linked->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
+
 }
 
 =item replace OLD_RECORD
@@ -460,6 +515,16 @@ 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.
@@ -468,23 +533,31 @@ Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
 
 sub cust_bill_pay {
   my $self = shift;
+  map { $_ } #return $self->num_cust_bill_pay unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
 }
 
 =item cust_credited
 
+=item cust_credit_bill
+
 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
 
 =cut
 
 sub cust_credited {
   my $self = shift;
+  map { $_ } #return $self->num_cust_credit_bill unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
   ;
 }
 
+sub cust_credit_bill {
+  shift->cust_credited(@_);
+}
+
 =item cust_bill_pay_pkgnum PKGNUM
 
 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
@@ -494,6 +567,7 @@ with matching pkgnum.
 
 sub cust_bill_pay_pkgnum {
   my( $self, $pkgnum ) = @_;
+  map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
                                 'pkgnum' => $pkgnum,
@@ -503,6 +577,8 @@ sub cust_bill_pay_pkgnum {
 
 =item cust_credited_pkgnum PKGNUM
 
+=item cust_credit_bill_pkgnum PKGNUM
+
 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
 with matching pkgnum.
 
@@ -510,6 +586,7 @@ with matching pkgnum.
 
 sub cust_credited_pkgnum {
   my( $self, $pkgnum ) = @_;
+  map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
   sort { $a->_date <=> $b->_date }
     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
                                    'pkgnum' => $pkgnum,
@@ -517,6 +594,10 @@ sub cust_credited_pkgnum {
            );
 }
 
+sub cust_credit_bill_pkgnum {
+  shift->cust_credited_pkgnum(@_);
+}
+
 =item tax
 
 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
@@ -711,6 +792,10 @@ text attachment arrayref, optional
 
 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>.
@@ -731,13 +816,19 @@ sub generate_email {
     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
   );
 
-  my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
+  my %opt = (
+    'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
+    'template'      => $args{'template'},
+    'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
+  );
+
+  my $cust_main = $self->cust_main;
 
   if (ref($args{'to'}) eq 'ARRAY') {
     $return{'to'} = $args{'to'};
   } else {
     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
-                           $self->cust_main->invoicing_list
+                           $cust_main->invoicing_list
                     ];
   }
 
@@ -771,7 +862,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $data = $args{'print_text'};
       } else {
-        $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
+        $data = [ $self->print_text(\%opt) ];
       }
 
     }
@@ -789,7 +880,7 @@ sub generate_email {
     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
 
     my $logo;
-    my $agentnum = $self->cust_main->agentnum;
+    my $agentnum = $cust_main->agentnum;
     if ( defined($args{'template'}) && length($args{'template'})
          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
        )
@@ -818,11 +909,7 @@ sub generate_email {
                          '    </title>',
                          '  </head>',
                          '  <body bgcolor="#e8e8e8">',
-                         $self->print_html({ time          => '',
-                                             template      => $args{'template'},
-                                             cid           => $content_id,
-                                             %cdrs,
-                                          }),
+                         $self->print_html({ 'cid'=>$content_id, %opt }),
                          '  </body>',
                          '</html>',
                        ],
@@ -831,7 +918,7 @@ sub generate_email {
     );
 
     my @otherparts = ();
-    if ( $self->cust_main->email_csv_cdr ) {
+    if ( $cust_main->email_csv_cdr ) {
 
       push @otherparts, build MIME::Entity
         'Type'        => 'text/csv',
@@ -870,7 +957,7 @@ sub generate_email {
 
       $related->add_part($image);
 
-      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
+      my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
 
       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
 
@@ -898,7 +985,7 @@ sub generate_email {
 
       #mime parts arguments a la MIME::Entity->build().
       $return{'mimeparts'} = [
-        { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
+        { $self->mimebuild_pdf(\%opt) }
       ];
     }
   
@@ -918,7 +1005,7 @@ sub generate_email {
       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
         $return{'body'} = $args{'print_text'};
       } else {
-        $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
+        $return{'body'} = [ $self->print_text(\%opt) ];
       }
 
     }
@@ -947,22 +1034,27 @@ sub mimebuild_pdf {
   );
 }
 
-=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+=item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
 
 Sends this invoice to the destinations configured for this customer: sends
 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to 
+four values for templatename, agentnum, invoice_from and amount.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
 
-AGENTNUM, if specified, means that this invoice will only be sent for customers
+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.
 
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<invoice_from>, if specified, overrides the default email invoice From: address.
 
-AMOUNT, if specified, only sends the invoice if the total amount owed on this
+I<amount>, if specified, only sends the invoice if the total amount owed on this
 invoice and all older invoices is greater than the specified amount.
 
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub queueable_send {
@@ -982,48 +1074,73 @@ sub queueable_send {
 
 sub send {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  if ( scalar(@_) && $_[0]  ) {
-    my $agentnums = ref($_[0]) ? shift : [ shift ];
-    return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
-  }
 
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from ||    #XXX should go away
-          $conf->config('invoice_from', $self->cust_main->agentnum )
-        );
+  my( $template, $invoice_from, $notice_name );
+  my $agentnums = '';
+  my $balance_over = 0;
 
-  my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    if ( $agentnums = $opt->{'agentnum'} ) {
+      $agentnums = [ $agentnums ] unless ref($agentnums);
+    }
+    $invoice_from = $opt->{'invoice_from'};
+    $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
+    $notice_name = $opt->{'notice_name'};
+  } else {
+    $template = scalar(@_) ? shift : '';
+    if ( scalar(@_) && $_[0]  ) {
+      $agentnums = ref($_[0]) ? shift : [ shift ];
+    }
+    $invoice_from = shift if scalar(@_);
+    $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
+  }
+
+  return 'N/A' unless ! $agentnums
+                   or grep { $_ == $self->cust_main->agentnum } @$agentnums;
 
   return ''
     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
 
+  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
+                    $conf->config('invoice_from', $self->cust_main->agentnum );
+
+  my %opt = (
+    'template'     => $template,
+    'invoice_from' => $invoice_from,
+    'notice_name'  => ( $notice_name || 'Invoice' ),
+  );
+
   my @invoicing_list = $self->cust_main->invoicing_list;
 
-  #$self->email_invoice($template, $invoice_from)
-  $self->email($template, $invoice_from)
+  #$self->email_invoice(\%opt)
+  $self->email(\%opt)
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
-  #$self->print_invoice($template)
-  $self->print($template)
+  #$self->print_invoice(\%opt)
+  $self->print(\%opt)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-  $self->fax_invoice($template)
+  $self->fax_invoice(\%opt)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
 
 }
 
-=item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
+=item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
 
 Emails this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a list of up to 
+two values for templatename and invoice_from.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<invoice_from>, if specified, overrides the default email invoice From: address.
 
-INVOICE_FROM, if specified, overrides the default email invoice From: address.
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
@@ -1045,14 +1162,21 @@ sub queueable_email {
 #sub email_invoice {
 sub email {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
-  my $invoice_from =
-    scalar(@_)
-      ? shift
-      : ( $self->_agent_invoice_from ||    #XXX should go away
-          $conf->config('invoice_from', $self->cust_main->agentnum )
-        );
 
+  my( $template, $invoice_from, $notice_name );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $invoice_from = $opt->{'invoice_from'};
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $invoice_from = shift if scalar(@_);
+    $notice_name = 'Invoice';
+  }
+
+  $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
+                    $conf->config('invoice_from', $self->cust_main->agentnum );
 
   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
                             $self->cust_main->invoicing_list;
@@ -1064,10 +1188,11 @@ sub email {
 
   my $error = send_email(
     $self->generate_email(
-      'from'       => $invoice_from,
-      'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
-      'subject'    => $subject,
-      'template'   => $template,
+      'from'        => $invoice_from,
+      'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+      'subject'     => $subject,
+      'template'    => $template,
+      'notice_name' => $notice_name,
     )
   );
   die "can't email invoice: $error\n" if $error;
@@ -1093,48 +1218,98 @@ sub email_subject {
   eval qq("$subject");
 }
 
-=item lpr_data [ TEMPLATENAME ]
+=item lpr_data HASHREF | [ TEMPLATE ]
 
 Returns the postscript or plaintext for this invoice as an arrayref.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional value
+for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 sub lpr_data {
-  my( $self, $template) = @_;
-  $conf->exists('invoice_latex')
-    ? [ $self->print_ps('', $template) ]
-    : [ $self->print_text('', $template) ];
+  my $self = shift;
+  my( $template, $notice_name );
+  if ( ref($_[0]) ) {
+    my $opt = shift;
+    $template = $opt->{'template'} || '';
+    $notice_name = $opt->{'notice_name'} || 'Invoice';
+  } else {
+    $template = scalar(@_) ? shift : '';
+    $notice_name = 'Invoice';
+  }
+
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
+  [ $self->$method( \%opt ) ];
 }
 
-=item print [ TEMPLATENAME ]
+=item print HASHREF | [ TEMPLATE ]
 
 Prints this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 #sub print_invoice {
 sub print {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
+  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';
+  }
 
-  do_print $self->lpr_data($template);
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  do_print $self->lpr_data(\%opt);
 }
 
-=item fax_invoice [ TEMPLATENAME ] 
+=item fax_invoice HASHREF | [ TEMPLATE ] 
 
 Faxes this invoice.
 
-TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+Options can be passed as a hashref (recommended) or as a single optional
+value for template.
+
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
 
 =cut
 
 sub fax_invoice {
   my $self = shift;
-  my $template = scalar(@_) ? shift : '';
+  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';
+  }
 
   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
     unless $conf->exists('invoice_latex');
@@ -1142,7 +1317,12 @@ sub fax_invoice {
   my $dialstring = $self->cust_main->getfield('fax');
   #Check $dialstring?
 
-  my $error = send_fax( 'docdata'    => $self->lpr_data($template),
+  my %opt = (
+    'template'    => $template,
+    'notice_name' => $notice_name,
+  );
+
+  my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
                         'dialstring' => $dialstring,
                       );
   die $error if $error;
@@ -1746,29 +1926,45 @@ sub _agent_invoice_from {
   $self->cust_main->agent_invoice_from;
 }
 
-=item print_text [ TIME [ , TEMPLATE ] ]
+=item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
 
 Returns an text invoice, as a list of lines.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_text {
-  my( $self, $today, $template, %opt ) = @_;
+  my $self = shift;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
 
   my %params = ( 'format' => 'template' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
-  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
 
   $self->print_generic( %params );
 }
 
-=item print_latex [ TIME [ , TEMPLATE ] ]
+=item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
 
 Internal method - returns a filename of a filled-in LaTeX template for this
 invoice (Note: add ".tex" to get the actual filename), and a filename of
@@ -1776,20 +1972,36 @@ an associated logo (with the .eps extension included).
 
 See print_ps and print_pdf for methods that return PostScript and PDF output.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time>, if specified, is used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_latex {
-  my( $self, $today, $template, %opt ) = @_;
+  my $self = shift;
+  my( $today, $template, %opt );
+  if ( ref($_[0]) ) {
+    %opt = %{ shift() };
+    $today = delete($opt{'time'}) || '';
+    $template = delete($opt{template}) || '';
+  } else {
+    ( $today, $template, %opt ) = @_;
+  }
 
   my %params = ( 'format' => 'latex' );
   $params{'time'} = $today if $today;
   $params{'template'} = $template if $template;
-  $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
+  $params{$_} = $opt{$_} 
+    foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
 
   $template ||= $self->_agent_template;
 
@@ -1827,7 +2039,7 @@ sub print_latex {
 
 }
 
-=item print_generic OPTIONS_HASH
+=item print_generic OPTION => VALUE ...
 
 Internal method - returns a filled-in template for this invoice as a scalar.
 
@@ -1849,10 +2061,12 @@ cid -
 
 unsquelch_cdr - overrides any per customer cdr squelching when true
 
+notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
-# (alignment?) problems to change them all to '%.2f' ?
+# (alignment in text invoice?) problems to change them all to '%.2f' ?
 sub print_generic {
 
   my( $self, %params ) = @_;
@@ -1909,6 +2123,7 @@ sub print_generic {
                  'smallfooter'   => sub { map "$_", @_ },
                  'returnaddress' => sub { map "$_", @_ },
                  'coupon'        => sub { map "$_", @_ },
+                 'summary'       => sub { map "$_", @_ },
                },
     'html'  => {
                  'notes' =>
@@ -1942,6 +2157,7 @@ sub print_generic {
                      }  @_
                    },
                  'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
                },
     'template' => {
                  'notes' =>
@@ -1972,6 +2188,7 @@ sub print_generic {
                      }  @_
                    },
                  'coupon'        => sub { "" },
+                 'summary'       => sub { "" },
                },
   );
 
@@ -2057,37 +2274,53 @@ sub print_generic {
   }
 
   my %invoice_data = (
+
+    #invoice from info
     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
-    'custnum'         => $cust_main->display_custnum,
+    'returnaddress'   => $returnaddress,
+    'agent'           => &$escape_function($cust_main->agent->agent),
+
+    #invoice info
     'invnum'          => $self->invnum,
     'date'            => time2str($date_format, $self->_date),
     'today'           => time2str('%b %o, %Y', $today),
-    'agent'           => &$escape_function($cust_main->agent->agent),
-    'agent_custid'    => &$escape_function($cust_main->agent_custid),
-    'payname'         => &$escape_function($cust_main->payname),
-    'company'         => &$escape_function($cust_main->company),
-    'address1'        => &$escape_function($cust_main->address1),
-    'address2'        => &$escape_function($cust_main->address2),
-    'city'            => &$escape_function($cust_main->city),
-    'state'           => &$escape_function($cust_main->state),
-    'zip'             => &$escape_function($cust_main->zip),
-    'fax'             => &$escape_function($cust_main->fax),
-    'returnaddress'   => $returnaddress,
-    #'quantity'        => 1,
     'terms'           => $self->terms,
     'template'        => $template, #params{'template'},
-    #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
-    # better hang on to conf_dir for a while
-    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
-    'page'            => 1,
-    'total_pages'     => 1,
+    'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
     'current_charges' => sprintf("%.2f", $self->charged),
     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
+
+    #customer info
+    'custnum'         => $cust_main->display_custnum,
+    'agent_custid'    => &$escape_function($cust_main->agent_custid),
+    ( map { $_ => &$escape_function($cust_main->$_()) } qw(
+      payname company address1 address2 city state zip fax
+    )),
+
+    #global config
     'ship_enable'     => $conf->exists('invoice-ship_address'),
     'unitprices'      => $conf->exists('invoice-unitprice'),
+    'smallernotes'    => $conf->exists('invoice-smallernotes'),
+    'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
+   
+    # better hang on to conf_dir for a while (for old templates)
+    'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+
+    #these are only used when doing paged plaintext
+    'page'            => 1,
+    'total_pages'     => 1,
+
   );
 
+  $invoice_data{finance_section} = '';
+  if ( $conf->config('finance_pkgclass') ) {
+    my $pkg_class =
+      qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
+    $invoice_data{finance_section} = $pkg_class->categoryname;
+  } 
+ $invoice_data{finance_amount} = '0.00';
+
   my $countrydefault = $conf->config('countrydefault') || 'US';
   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
   foreach ( qw( contact company address1 address2 city state zip country fax) ){
@@ -2134,11 +2367,19 @@ sub print_generic {
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
   #my $balance_due = $self->owed + $pr_total - $cr_total;
   my $balance_due = $self->owed + $pr_total;
+  $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
+  $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   my $agentnum = $self->cust_main->agentnum;
 
+  my $summarypage = '';
+  if ( $conf->exists('invoice_usesummary', $agentnum) ) {
+    $summarypage = 1;
+  }
+  $invoice_data{'summarypage'} = $summarypage;
+
   #do variable substitution in notes, footer, smallfooter
   foreach my $include (qw( notes footer smallfooter coupon )) {
 
@@ -2198,6 +2439,7 @@ sub print_generic {
                             'template' => '',
                           );
   my $other_money_char = $other_money_chars{$format};
+  $invoice_data{'dollar'} = $other_money_char;
 
   my @detail_items = ();
   my @total_items = ();
@@ -2212,21 +2454,27 @@ sub print_generic {
   my $previous_section = { 'description' => 'Previous Charges',
                            'subtotal'    => $other_money_char.
                                             sprintf('%.2f', $pr_total),
+                           'summarized'  => $summarypage ? 'Y' : '',
                          };
 
   my $taxtotal = 0;
   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
-                      'subtotal'    => $taxtotal }; # adjusted below
+                      'subtotal'    => $taxtotal,   # adjusted below
+                      'summarized'  => $summarypage ? 'Y' : '',
+                    };
 
   my $adjusttotal = 0;
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
-                         'subtotal'    => 0 }; # adjusted below
+                         'subtotal'    => 0,   # adjusted below
+                         'summarized'  => $summarypage ? 'Y' : '',
+                       };
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
   my $late_sections = [];
   if ( $multisection ) {
-    push @sections, $self->_items_sections( $late_sections );
+    push @sections,
+      $self->_items_sections( $late_sections, $summarypage, $escape_function );
   }else{
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
@@ -2271,6 +2519,10 @@ sub print_generic {
 
   foreach my $section (@sections, @$late_sections) {
 
+    $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
+      if ( $invoice_data{finance_section} &&
+           $section->{'description'} eq $invoice_data{finance_section} );
+
     $section->{'subtotal'} = $other_money_char.
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
@@ -2287,6 +2539,7 @@ sub print_generic {
     $options{'escape_function'} = $escape_function;
     $options{'format_function'} = sub { () } unless $unsquelched;
     $options{'unsquelched'} = $unsquelched;
+    $options{'summary_page'} = $summarypage;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
@@ -2325,6 +2578,9 @@ sub print_generic {
   
   }
   
+  $invoice_data{current_less_finance} =
+    sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
+
   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
     unshift @sections, $previous_section if $pr_total;
   }
@@ -2496,7 +2752,11 @@ sub print_generic {
       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
       $total->{'total_amount'} =
         &$embolden_function(
-          $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
+          $other_money_char. sprintf('%.2f', $summarypage 
+                                               ? $self->charged +
+                                                 $self->billing_balance
+                                               : $self->owed + $pr_total
+                                    )
         );
       if ( $multisection ) {
         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
@@ -2515,6 +2775,49 @@ sub print_generic {
       if $unsquelched;
   }
 
+  my @includelist = ();
+  push @includelist, 'summary' if $summarypage;
+  foreach my $include ( @includelist ) {
+
+    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
+    my @inc_src;
+
+    if ( length( $conf->config($inc_file, $agentnum) ) ) {
+
+      @inc_src = $conf->config($inc_file, $agentnum);
+
+    } else {
+
+      $inc_file = $conf->key_orbase("invoice_latex$include", $template);
+
+      my $convert_map = $convert_maps{$format}{$include};
+
+      @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
+                       s/--\@\]/$delimiters{$format}[1]/g;
+                       $_;
+                     } 
+                 &$convert_map( $conf->config($inc_file, $agentnum) );
+
+    }
+
+    my $inc_tt = new Text::Template (
+      TYPE       => 'ARRAY',
+      SOURCE     => [ map "$_\n", @inc_src ],
+      DELIMITERS => $delimiters{$format},
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
+
+    unless ( $inc_tt->compile() ) {
+      my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
+      warn $error. "Template:\n". join('', map "$_\n", @inc_src);
+      die $error;
+    }
+
+    $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
+
+    $invoice_data{$include} =~ s/\n+$//
+      if ($format eq 'latex');
+  }
+
   $invoice_lines = 0;
   my $wasfunc = 0;
   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
@@ -2565,15 +2868,20 @@ sub print_generic {
   }
 }
 
-=item print_ps [ TIME [ , TEMPLATE ] ]
+=item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_ps {
@@ -2586,15 +2894,22 @@ sub print_ps {
   $ps;
 }
 
-=item print_pdf [ TIME [ , TEMPLATE ] ]
+=item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an PDF invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+Options can be passed as a hashref (recommended) or as a list of time, template
+and then any key/value pairs for any other options.
+
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
 =cut
 
 sub print_pdf {
@@ -2607,16 +2922,20 @@ sub print_pdf {
   $pdf;
 }
 
-=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
+=item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
 
 Returns an HTML invoice, as a scalar.
 
-TIME an optional value used to control the printing of overdue messages.  The
+I<time> an optional value used to control the printing of overdue messages.  The
 default is now.  It isn't the date of the invoice; that's the `_date' field.
 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
 L<Time::Local> and L<Date::Parse> for conversion functions.
 
-CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
+I<template>, if specified, is the name of a suffix for alternate invoices.
+
+I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
+
+I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
 when emailing the invoice as part of a multipart/related MIME email.
 
 =cut
@@ -2624,7 +2943,7 @@ when emailing the invoice as part of a multipart/related MIME email.
 sub print_html {
   my $self = shift;
   my %params;
-  if ( ref $_[0]  ) {
+  if ( ref($_[0]) ) {
     %params = %{ shift() }; 
   }else{
     $params{'time'} = shift;
@@ -2682,10 +3001,10 @@ sub _translate_old_latex_format {
         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
         push @template, "    \$OUT .= '$line_item_line';";
       }
-  
+
       push @template, '}',
                       '--@]';
-
+      #' doh, gvim
     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
 
       push @template, '[@--',
@@ -2719,11 +3038,12 @@ sub _translate_old_latex_format {
 sub terms {
   my $self = shift;
 
-  #check for an invoice- specific override (eventually)
+  #check for an invoice-specific override
+  return $self->invoice_terms if $self->invoice_terms;
   
   #check for a customer- specific override
-  return $self->cust_main->invoice_terms
-    if $self->cust_main->invoice_terms;
+  my $cust_main = $self->cust_main;
+  return $cust_main->invoice_terms if $cust_main->invoice_terms;
 
   #use configured default
   $conf->config('invoice_default_terms') || '';
@@ -2788,78 +3108,116 @@ sub _date_pretty {
   time2str('%x', $self->_date);
 }
 
+use vars qw(%pkg_category_cache);
 sub _items_sections {
   my $self = shift;
   my $late = shift;
+  my $summarypage = shift;
+  my $escape = shift;
 
-  my %s = ();
-  my %l = ();
+  my %subtotal = ();
+  my %late_subtotal = ();
+  my %not_tax = ();
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
   {
 
-    if ( $cust_bill_pkg->pkgnum > 0 ) {
       my $usage = $cust_bill_pkg->usage;
 
       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
-        my $desc = $display->section;
-        my $type = $display->type;
+        next if ( $display->summary && $summarypage );
+
+        my $section = $display->section;
+        my $type    = $display->type;
+
+        $not_tax{$section} = 1
+          unless $cust_bill_pkg->pkgnum == 0;
 
-        if ( $display->post_total ) {
+        if ( $display->post_total && !$summarypage ) {
           if (! $type || $type eq 'S') {
-            $l{$desc} += $cust_bill_pkg->setup
-              if ( $cust_bill_pkg->setup != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0;
           }
 
           if (! $type) {
-            $l{$desc} += $cust_bill_pkg->recur
-              if ( $cust_bill_pkg->recur != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0;
           }
 
           if ($type && $type eq 'R') {
-            $l{$desc} += $cust_bill_pkg->recur - $usage
-              if ( $cust_bill_pkg->recur != 0 );
+            $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0;
           }
           
           if ($type && $type eq 'U') {
-            $l{$desc} += $usage;
+            $late_subtotal{$section} += $usage;
           }
 
         } else {
+
+          next if $cust_bill_pkg->pkgnum == 0 && ! $section;
+
           if (! $type || $type eq 'S') {
-            $s{$desc} += $cust_bill_pkg->setup
-              if ( $cust_bill_pkg->setup != 0 );
+            $subtotal{$section} += $cust_bill_pkg->setup
+              if $cust_bill_pkg->setup != 0;
           }
 
           if (! $type) {
-            $s{$desc} += $cust_bill_pkg->recur
-              if ( $cust_bill_pkg->recur != 0 );
+            $subtotal{$section} += $cust_bill_pkg->recur
+              if $cust_bill_pkg->recur != 0;
           }
 
           if ($type && $type eq 'R') {
-            $s{$desc} += $cust_bill_pkg->recur - $usage
-              if ( $cust_bill_pkg->recur != 0 );
+            $subtotal{$section} += $cust_bill_pkg->recur - $usage
+              if $cust_bill_pkg->recur != 0;
           }
           
           if ($type && $type eq 'U') {
-            $s{$desc} += $usage;
+            $subtotal{$section} += $usage;
           }
 
         }
 
       }
 
-    }
-
   }
 
-  push @$late, map { { 'description' => $_,
-                       'subtotal'    => $l{$_},
+  %pkg_category_cache = ();
+
+  push @$late, map { { 'description' => &{$escape}($_),
+                       'subtotal'    => $late_subtotal{$_},
                        'post_total'  => 1,
-                   } } sort keys %l;
+                   } }
+                 sort _categorysort keys %late_subtotal;
+
+  my @sections;
+  if ( $summarypage ) {
+    @sections = grep { exists($subtotal{$_}) || ! _pkg_category{$_}->disabled }
+                keys %pkg_category_cache;
+  } else {
+    @sections = keys %subtotal;
+  }
+
+  map { { 'description' => &{$escape}($_),
+          'subtotal'    => $subtotal{$_},
+          'summarized'  => $not_tax{$_} ? '' : 'Y',
+          'tax_section' => $not_tax{$_} ? '' : 'Y',
+        }
+      }
+    sort _categorysort @sections;
+
+}
+
+#helper subs for above
 
-  map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
+sub _categorysort {
+  _pkg_category($a)->weight <=> _pkg_category($b)->weight;
+}
 
+sub _pkg_category {
+  my $categoryname = shift;
+  $pkg_category_cache{$categoryname} ||=
+    qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
 }
 
 sub _items {
@@ -2940,6 +3298,7 @@ sub _items_cust_bill_pkg {
   my $format_function = $opt{format_function} || '';
   my $unsquelched = $opt{unsquelched} || '';
   my $section = $opt{section}->{description} if $opt{section};
+  my $summary_page = $opt{summary_page} || '';
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
@@ -2959,6 +3318,7 @@ sub _items_cust_bill_pkg {
                                  ? $_->section eq $section
                                  : 1
                                }
+                          grep { $_->summary || !$summary_page }
                           $cust_bill_pkg->cust_bill_pkg_display
                         )
     {
@@ -3015,7 +3375,8 @@ sub _items_cust_bill_pkg {
         {
 
           my $is_summary = $display->summary;
-          my $description = $is_summary ? "Usage charges" : $desc;
+          my $description = ($is_summary && $type && $type eq 'U')
+                            ? "Usage charges" : $desc;
 
           unless ( $conf->exists('disable_line_item_date_ranges') ) {
             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
@@ -3037,7 +3398,7 @@ sub _items_cust_bill_pkg {
             unless $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
-                || $is_summary;
+                || $is_summary && $type && $type eq 'U';
 
           push @d, $cust_bill_pkg->details(%details_opt)
             unless ($is_summary || $type && $type eq 'R');