add some dollar signs RT8704
[freeside.git] / FS / FS / cust_bill.pm
index cf339a8..4bd9aa1 100644 (file)
@@ -1,7 +1,7 @@
 package FS::cust_bill;
 
 use strict;
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $DEBUG $me $conf $money_char );
+use vars qw( @ISA $DEBUG $me $conf $money_char $date_format $rdate_format );
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use List::Util qw(min max);
 use vars qw( $invoice_lines @buf ); #yuck
 use Fcntl qw(:flock); #for spool_csv
 use List::Util qw(min max);
@@ -34,16 +34,20 @@ use FS::cust_bill_pay;
 use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
 use FS::cust_bill_pay_batch;
 use FS::part_bill_event;
 use FS::payby;
+use FS::bill_batch;
+use FS::cust_bill_batch;
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
 
 @ISA = qw( FS::cust_main_Mixin FS::Record );
 
-$DEBUG = 1;
+$DEBUG = 0;
 $me = '[FS::cust_bill]';
 
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
 $me = '[FS::cust_bill]';
 
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
-  $money_char = $conf->config('money_char') || '$';  
+  $money_char   = $conf->config('money_char')  || '$';  
+  $date_format  = $conf->config('date_format') || '%x';  
+  $rdate_format = $conf->config('date_format') || '%m/%d/%Y';  
 } );
 
 =head1 NAME
 } );
 
 =head1 NAME
@@ -354,11 +358,24 @@ this invoice.
 
 sub cust_pkg {
   my $self = shift;
 
 sub cust_pkg {
   my $self = shift;
-  my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
+  my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
+                     $self->cust_bill_pkg;
   my %saw = ();
   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
 }
 
   my %saw = ();
   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
 }
 
+=item no_auto
+
+Returns true if any of the packages (or their definitions) corresponding to the
+line items for this invoice have the no_auto flag set.
+
+=cut
+
+sub no_auto {
+  my $self = shift;
+  grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
+}
+
 =item open_cust_bill_pkg
 
 Returns the open line items for this invoice.
 =item open_cust_bill_pkg
 
 Returns the open line items for this invoice.
@@ -1285,7 +1302,13 @@ sub print {
     'notice_name' => $notice_name,
   );
 
     'notice_name' => $notice_name,
   );
 
-  do_print $self->lpr_data(\%opt);
+  if($conf->exists('invoice_print_pdf')) {
+    # Add the invoice to the current batch.
+    $self->batch_invoice(\%opt);
+  }
+  else {
+    do_print $self->lpr_data(\%opt);
+  }
 }
 
 =item fax_invoice HASHREF | [ TEMPLATE ] 
 }
 
 =item fax_invoice HASHREF | [ TEMPLATE ] 
@@ -1331,6 +1354,23 @@ sub fax_invoice {
 
 }
 
 
 }
 
+=item batch_invoice [ HASHREF ]
+
+Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
+isn't an open batch, one will be created.
+
+=cut
+
+sub batch_invoice {
+  my ($self, $opt) = @_;
+  my $batch = FS::bill_batch->get_open_batch;
+  my $cust_bill_batch = FS::cust_bill_batch->new({
+      batchnum => $batch->batchnum,
+      invnum   => $self->invnum,
+  });
+  return $cust_bill_batch->insert($opt);
+}
+
 =item ftp_invoice [ TEMPLATENAME ] 
 
 Sends this invoice data via FTP.
 =item ftp_invoice [ TEMPLATENAME ] 
 
 Sends this invoice data via FTP.
@@ -1897,6 +1937,14 @@ sub realtime_bop {
   $cust_main->realtime_bop($method, $amount,
     'description' => $description,
     'invnum'      => $self->invnum,
   $cust_main->realtime_bop($method, $amount,
     'description' => $description,
     'invnum'      => $self->invnum,
+#this didn't do what we want, it just calls apply_payments_and_credits
+#    'apply'       => 1,
+    'apply_to_invoice' => 1,
+ #what we want:
+ #this changes application behavior: auto payments
+                        #triggered against a specific invoice are now applied
+                        #to that invoice instead of oldest open.
+                        #seem okay to me...
   );
 
 }
   );
 
 }
@@ -2276,11 +2324,13 @@ sub print_generic {
 
   }
 
 
   }
 
+  my $agentnum = $self->cust_main->agentnum;
+
   my %invoice_data = (
 
     #invoice from info
   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",
+    'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
+    'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
     'returnaddress'   => $returnaddress,
     'agent'           => &$escape_function($cust_main->agent->agent),
 
     'returnaddress'   => $returnaddress,
     'agent'           => &$escape_function($cust_main->agent->agent),
 
@@ -2292,7 +2342,7 @@ sub print_generic {
     'template'        => $template, #params{'template'},
     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
     'current_charges' => sprintf("%.2f", $self->charged),
     'template'        => $template, #params{'template'},
     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
     'current_charges' => sprintf("%.2f", $self->charged),
-    'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
+    'duedate'         => $self->due_date2str($rdate_format), #date_format?
 
     #customer info
     'custnum'         => $cust_main->display_custnum,
 
     #customer info
     'custnum'         => $cust_main->display_custnum,
@@ -2306,7 +2356,21 @@ sub print_generic {
     'unitprices'      => $conf->exists('invoice-unitprice'),
     'smallernotes'    => $conf->exists('invoice-smallernotes'),
     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
     'unitprices'      => $conf->exists('invoice-unitprice'),
     'smallernotes'    => $conf->exists('invoice-smallernotes'),
     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
+    'balance_due_below_line' => $conf->exists('balance_due_below_line'),
    
    
+    #layout info -- would be fancy to calc some of this and bury the template
+    #               here in the code
+    'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
+    'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
+    'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
+    'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
+    'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
+    'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
+    'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
+    'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
+    'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
+    'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
+
     # better hang on to conf_dir for a while (for old templates)
     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
 
     # better hang on to conf_dir for a while (for old templates)
     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
 
@@ -2375,8 +2439,6 @@ sub print_generic {
   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
 
   $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;
   my $summarypage = '';
   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
     $summarypage = 1;
@@ -2459,27 +2521,48 @@ sub print_generic {
                                             sprintf('%.2f', $pr_total),
                            'summarized'  => $summarypage ? 'Y' : '',
                          };
                                             sprintf('%.2f', $pr_total),
                            'summarized'  => $summarypage ? 'Y' : '',
                          };
+  $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
+    join(' / ', map { $cust_main->balance_date_range(@$_) }
+                $self->_prior_month30s
+        )
+    if $conf->exists('invoice_include_aging');
 
   my $taxtotal = 0;
   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
                       'subtotal'    => $taxtotal,   # adjusted below
                       'summarized'  => $summarypage ? 'Y' : '',
                     };
 
   my $taxtotal = 0;
   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
                       'subtotal'    => $taxtotal,   # adjusted below
                       'summarized'  => $summarypage ? 'Y' : '',
                     };
+  my $tax_weight = _pkg_category($tax_section->{description})
+                        ? _pkg_category($tax_section->{description})->weight
+                        : 0;
+  $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
+  $tax_section->{'sort_weight'} = $tax_weight;
+
 
   my $adjusttotal = 0;
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
                          'subtotal'    => 0,   # adjusted below
                          'summarized'  => $summarypage ? 'Y' : '',
                        };
 
   my $adjusttotal = 0;
   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
                          'subtotal'    => 0,   # adjusted below
                          'summarized'  => $summarypage ? 'Y' : '',
                        };
+  my $adjust_weight = _pkg_category($adjust_section->{description})
+                        ? _pkg_category($adjust_section->{description})->weight
+                        : 0;
+  $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
+  $adjust_section->{'sort_weight'} = $adjust_weight;
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
 
   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
+  $invoice_data{'multisection'} = $multisection;
   my $late_sections = [];
   my $late_sections = [];
+  my $extra_sections = [];
+  my $extra_lines = ();
   if ( $multisection ) {
   if ( $multisection ) {
-    my ($extra_sections, $extra_lines) =
+    ($extra_sections, $extra_lines) =
       $self->_items_extra_usage_sections($escape_function, $format)
       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
 
       $self->_items_extra_usage_sections($escape_function, $format)
       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
 
+    push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
+
     push @detail_items, @$extra_lines if $extra_lines;
     push @sections,
       $self->_items_sections( $late_sections,      # this could stand a refactor
     push @detail_items, @$extra_lines if $extra_lines;
     push @sections,
       $self->_items_sections( $late_sections,      # this could stand a refactor
@@ -2538,6 +2621,12 @@ sub print_generic {
 
   foreach my $section (@sections, @$late_sections) {
 
 
   foreach my $section (@sections, @$late_sections) {
 
+    # begin some normalization
+    $section->{'subtotal'} = $section->{'amount'}
+      if $multisection
+         && !exists($section->{subtotal})
+         && exists($section->{amount});
+
     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
       if ( $invoice_data{finance_section} &&
            $section->{'description'} eq $invoice_data{finance_section} );
     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
       if ( $invoice_data{finance_section} &&
            $section->{'description'} eq $invoice_data{finance_section} );
@@ -2546,7 +2635,7 @@ sub print_generic {
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
 
                              sprintf('%.2f', $section->{'subtotal'})
       if $multisection;
 
-    # begin some normalization
+    # continue some normalization
     $section->{'amount'}   = $section->{'subtotal'}
       if $multisection;
 
     $section->{'amount'}   = $section->{'subtotal'}
       if $multisection;
 
@@ -2557,6 +2646,7 @@ sub print_generic {
                  );
     }
 
                  );
     }
 
+    my $multilocation = scalar($cust_main->cust_location); #too expensive?
     my %options = ();
     $options{'section'} = $section if $multisection;
     $options{'format'} = $format;
     my %options = ();
     $options{'section'} = $section if $multisection;
     $options{'format'} = $format;
@@ -2564,6 +2654,9 @@ sub print_generic {
     $options{'format_function'} = sub { () } unless $unsquelched;
     $options{'unsquelched'} = $unsquelched;
     $options{'summary_page'} = $summarypage;
     $options{'format_function'} = sub { () } unless $unsquelched;
     $options{'unsquelched'} = $unsquelched;
     $options{'summary_page'} = $summarypage;
+    $options{'skip_usage'} =
+      scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
+    $options{'multilocation'} = $multilocation;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
@@ -2605,7 +2698,9 @@ sub print_generic {
   $invoice_data{current_less_finance} =
     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
 
   $invoice_data{current_less_finance} =
     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
 
-  if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+  if ( $multisection && !$conf->exists('disable_previous_balance')
+    || $conf->exists('previous_balance-summary_only') )
+  {
     unshift @sections, $previous_section if $pr_total;
   }
 
     unshift @sections, $previous_section if $pr_total;
   }
 
@@ -2672,32 +2767,34 @@ sub print_generic {
 
   {
     my $total = {};
 
   {
     my $total = {};
-    $total->{'total_item'} = &$embolden_function('Total');
+    my $item = 'Total';
+    $item = $conf->config('previous_balance-exclude_from_total')
+         || 'Total New Charges'
+      if $conf->exists('previous_balance-exclude_from_total');
+    my $amount = $self->charged +
+                   ( $conf->exists('disable_previous_balance') ||
+                     $conf->exists('previous_balance-exclude_from_total')
+                     ? 0
+                     : $pr_total
+                   );
+    $total->{'total_item'} = &$embolden_function($item);
     $total->{'total_amount'} =
     $total->{'total_amount'} =
-      &$embolden_function(
-        $other_money_char.
-        sprintf( '%.2f',
-                 $self->charged + ( $conf->exists('disable_previous_balance')
-                                    ? 0
-                                    : $pr_total
-                                  )
-               )
-      );
+      &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
     if ( $multisection ) {
     if ( $multisection ) {
-      $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
-                                      sprintf('%.2f', $self->charged );
+      if ( $adjust_section->{'sort_weight'} ) {
+        $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
+          sprintf("%.2f", ($self->billing_balance || 0) );
+      } else {
+        $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
+                                        sprintf('%.2f', $self->charged );
+      } 
     }else{
       push @total_items, $total;
     }
     push @buf,['','-----------'];
     }else{
       push @total_items, $total;
     }
     push @buf,['','-----------'];
-    push @buf,['Total Charges',
+    push @buf,[$item,
                $money_char.
                $money_char.
-               sprintf( '%10.2f', $self->charged +
-                                    ( $conf->exists('disable_previous_balance')
-                                        ? 0
-                                        : $pr_total
-                                    )
-                      )
+               sprintf( '%10.2f', $amount )
               ];
     push @buf,['',''];
   }
               ];
     push @buf,['',''];
   }
@@ -2768,7 +2865,8 @@ sub print_generic {
     if ( $multisection ) {
       $adjust_section->{'subtotal'} = $other_money_char.
                                       sprintf('%.2f', $adjusttotal);
     if ( $multisection ) {
       $adjust_section->{'subtotal'} = $other_money_char.
                                       sprintf('%.2f', $adjusttotal);
-      push @sections, $adjust_section;
+      push @sections, $adjust_section
+        unless $adjust_section->{sort_weight};
     }
 
     { 
     }
 
     { 
@@ -2782,7 +2880,7 @@ sub print_generic {
                                                : $self->owed + $pr_total
                                     )
         );
                                                : $self->owed + $pr_total
                                     )
         );
-      if ( $multisection ) {
+      if ( $multisection && !$adjust_section->{sort_weight} ) {
         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
                                          $total->{'total_amount'};
       }else{
         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
                                          $total->{'total_amount'};
       }else{
@@ -2795,6 +2893,18 @@ sub print_generic {
   }
 
   if ( $multisection ) {
   }
 
   if ( $multisection ) {
+    if ($conf->exists('svc_phone_sections')) {
+      my $total;
+      $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
+      $total->{'total_amount'} =
+        &$embolden_function(
+          $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
+        );
+      my $last_section = pop @sections;
+      $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                     $total->{'total_amount'};
+      push @sections, $last_section;
+    }
     push @sections, @$late_sections
       if $unsquelched;
   }
     push @sections, @$late_sections
       if $unsquelched;
   }
@@ -2892,6 +3002,22 @@ sub print_generic {
   }
 }
 
   }
 }
 
+# helper routine for generating date ranges
+sub _prior_month30s {
+  my $self = shift;
+  my @ranges = (
+   [ 1,       2592000 ], # 0-30 days ago
+   [ 2592000, 5184000 ], # 30-60 days ago
+   [ 5184000, 7776000 ], # 60-90 days ago
+   [ 7776000, 0       ], # 90+   days ago
+  );
+
+  map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
+          $_->[1] ? $self->_date - $_->[1] - 1 : '',
+      ] }
+  @ranges;
+}
+
 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
 
 Returns an postscript invoice, as a scalar.
@@ -3092,7 +3218,7 @@ sub balance_due_msg {
   my $msg = 'Balance Due';
   return $msg unless $self->terms;
   if ( $self->due_date ) {
   my $msg = 'Balance Due';
   return $msg unless $self->terms;
   if ( $self->due_date ) {
-    $msg .= ' - Please pay by '. $self->due_date2str('%x');
+    $msg .= ' - Please pay by '. $self->due_date2str($date_format);
   } elsif ( $self->terms ) {
     $msg .= ' - '. $self->terms;
   }
   } elsif ( $self->terms ) {
     $msg .= ' - '. $self->terms;
   }
@@ -3104,7 +3230,7 @@ sub balance_due_date {
   my $duedate = '';
   if (    $conf->exists('invoice_default_terms') 
        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
   my $duedate = '';
   if (    $conf->exists('invoice_default_terms') 
        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
-    $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
+    $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
   }
   $duedate;
 }
   }
   $duedate;
 }
@@ -3129,7 +3255,7 @@ Returns a string with the date, for example: "3/20/2008"
 
 sub _date_pretty {
   my $self = shift;
 
 sub _date_pretty {
   my $self = shift;
-  time2str('%x', $self->_date);
+  time2str($date_format, $self->_date);
 }
 
 use vars qw(%pkg_category_cache);
 }
 
 use vars qw(%pkg_category_cache);
@@ -3176,7 +3302,8 @@ sub _items_sections {
           }
           
           if ($type && $type eq 'U') {
           }
           
           if ($type && $type eq 'U') {
-            $late_subtotal{$section} += $usage;
+            $late_subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
           }
 
         } else {
           }
 
         } else {
@@ -3199,7 +3326,8 @@ sub _items_sections {
           }
           
           if ($type && $type eq 'U') {
           }
           
           if ($type && $type eq 'U') {
-            $subtotal{$section} += $usage;
+            $subtotal{$section} += $usage
+              unless scalar(@$extra_sections);
           }
 
         }
           }
 
         }
@@ -3213,8 +3341,11 @@ sub _items_sections {
   push @$late, map { { 'description' => &{$escape}($_),
                        'subtotal'    => $late_subtotal{$_},
                        'post_total'  => 1,
   push @$late, map { { 'description' => &{$escape}($_),
                        'subtotal'    => $late_subtotal{$_},
                        'post_total'  => 1,
-                       'sort_weight' => _pkg_category($_)->weight,
-                       (_pkg_category($_)->condense
+                       'sort_weight' => ( _pkg_category($_)
+                                            ? _pkg_category($_)->weight
+                                            : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
                                            ? $self->_condense_section($format)
                                            : ()
                        ),
                                            ? $self->_condense_section($format)
                                            : ()
                        ),
@@ -3233,8 +3364,11 @@ sub _items_sections {
                       'subtotal'    => $subtotal{$_},
                       'summarized'  => $not_tax{$_} ? '' : 'Y',
                       'tax_section' => $not_tax{$_} ? '' : 'Y',
                       'subtotal'    => $subtotal{$_},
                       'summarized'  => $not_tax{$_} ? '' : 'Y',
                       'tax_section' => $not_tax{$_} ? '' : 'Y',
-                      'sort_weight' => _pkg_category($_)->weight,
-                       (_pkg_category($_)->condense
+                      'sort_weight' => ( _pkg_category($_)
+                                           ? _pkg_category($_)->weight
+                                           : 0
+                                       ),
+                       ((_pkg_category($_) && _pkg_category($_)->condense)
                                            ? $self->_condense_section($format)
                                            : ()
                        ),
                                            ? $self->_condense_section($format)
                                            : ()
                        ),
@@ -3263,7 +3397,9 @@ my %condensed_format = (
   'fields' => [
                 sub { shift->{description} },
                 sub { shift->{quantity} },
   'fields' => [
                 sub { shift->{description} },
                 sub { shift->{quantity} },
-                sub { shift->{amount} },
+                sub { my($href, %opt) = @_;
+                      ($opt{dollar} || ''). $href->{amount};
+                    },
               ],
   'align'  => [ qw( l r r ) ],
   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
               ],
   'align'  => [ qw( l r r ) ],
   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
@@ -3337,6 +3473,7 @@ sub _condensed_description_generator {
   my ( $f, $prefix, $suffix, $separator, $column ) =
     _condensed_generator_defaults($format);
 
   my ( $f, $prefix, $suffix, $separator, $column ) =
     _condensed_generator_defaults($format);
 
+  my $money_char = '$';
   if ($format eq 'latex') {
     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
     $suffix = '\\\\';
   if ($format eq 'latex') {
     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
     $suffix = '\\\\';
@@ -3345,6 +3482,7 @@ sub _condensed_description_generator {
       sub { my ($d,$a,$s,$w) = @_;
             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
           };
       sub { my ($d,$a,$s,$w) = @_;
             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
           };
+    $money_char = '\\dollar';
   }elsif ( $format eq 'html' ) {
     $prefix = '"><td align="center"></td>';
     $suffix = '';
   }elsif ( $format eq 'html' ) {
     $prefix = '"><td align="center"></td>';
     $suffix = '';
@@ -3353,16 +3491,22 @@ sub _condensed_description_generator {
       sub { my ($d,$a,$s,$w) = @_;
             return qq!<td align="$html_align{$a}">$d</td>!;
       };
       sub { my ($d,$a,$s,$w) = @_;
             return qq!<td align="$html_align{$a}">$d</td>!;
       };
+    #$money_char = $conf->config('money_char') || '$';
+    $money_char = '';  # this is madness
   }
 
   sub {
   }
 
   sub {
-    my @args = @_;
+    #my @args = @_;
+    my $href = shift;
     my @result = ();
 
     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
     my @result = ();
 
     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
-      push @result, &{$column}( &{$f->{fields}->[$i]}(@args),
-                                map { $f->{$_}->[$i] } qw(align span width)
-                              );
+      my $dollar = '';
+      $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
+      push @result,
+        &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
+                    map { $f->{$_}->[$i] } qw(align span width)
+                  );
     }
 
     $prefix. join( $separator, @result ). $suffix;
     }
 
     $prefix. join( $separator, @result ). $suffix;
@@ -3607,6 +3751,9 @@ sub _items_svc_phone_sections {
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
     next unless $cust_bill_pkg->pkgnum > 0;
 
   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
     next unless $cust_bill_pkg->pkgnum > 0;
 
+    my @header = $cust_bill_pkg->details_header;
+    next unless scalar(@header);
+
     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
       my $phonenum = $detail->phonenum;
     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
 
       my $phonenum = $detail->phonenum;
@@ -3655,6 +3802,7 @@ sub _items_svc_phone_sections {
           'duration' => 0,
           'sort_weight' => $usage_class{$detail->classnum}->weight,
           'phonenum' => $phonenum,
           'duration' => 0,
           'sort_weight' => $usage_class{$detail->classnum}->weight,
           'phonenum' => $phonenum,
+          'header'  => [ @header ],
         };
       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
       $sections{"$phonenum $line"}{calls}++;
         };
       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
       $sections{"$phonenum $line"}{calls}++;
@@ -3685,11 +3833,17 @@ sub _items_svc_phone_sections {
 
   my %sectionmap = ();
   my $simple = new FS::usage_class { format => 'simple' }; #bleh
 
   my %sectionmap = ();
   my $simple = new FS::usage_class { format => 'simple' }; #bleh
-  my $minimal = new FS::usage_class { format => 'minimal' }; #bleh
   foreach ( keys %sections ) {
   foreach ( keys %sections ) {
+    my @header = @{ $sections{$_}{header} || [] };
+    my $usage_simple =
+      new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
-    my $usage_class = $summary ? $simple : $minimal;
+    my $usage_class = $summary ? $simple : $usage_simple;
     my $ending = $summary ? ' usage charges' : '';
     my $ending = $summary ? ' usage charges' : '';
+    my %gen_opt = ();
+    unless ($summary) {
+      $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
+    }
     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
                         'amount'    => $sections{$_}{amount},    #subtotal
                         'calls'       => $sections{$_}{calls},
     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
                         'amount'    => $sections{$_}{amount},    #subtotal
                         'calls'       => $sections{$_}{calls},
@@ -3698,8 +3852,9 @@ sub _items_svc_phone_sections {
                         'tax_section' => '',
                         'phonenum'    => $sections{$_}{phonenum},
                         'sort_weight' => $sections{$_}{sort_weight},
                         'tax_section' => '',
                         'phonenum'    => $sections{$_}{phonenum},
                         'sort_weight' => $sections{$_}{sort_weight},
+                        'post_total'  => $summary, #inspire pagebreak
                         (
                         (
-                          ( map { $_ => $usage_class->$_($format) }
+                          ( map { $_ => $usage_class->$_($format, %gen_opt) }
                             qw( description_generator
                                 header_generator
                                 total_generator
                             qw( description_generator
                                 header_generator
                                 total_generator
@@ -3753,9 +3908,11 @@ sub _items_previous {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my @b = ();
   foreach ( @pr_cust_bill ) {
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
   my @b = ();
   foreach ( @pr_cust_bill ) {
+    my $date = $conf->exists('invoice_show_prior_due_date')
+               ? 'due '. $_->due_date2str($date_format)
+               : time2str($date_format, $_->_date);
     push @b, {
     push @b, {
-      'description' => 'Previous Balance, Invoice #'. $_->invnum. 
-                       ' ('. time2str('%x',$_->_date). ')',
+      'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
       'amount'      => sprintf("%.2f", $_->owed),
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
       'amount'      => sprintf("%.2f", $_->owed),
@@ -3806,12 +3963,12 @@ sub _items_pkg {
 }
 
 sub _taxsort {
 }
 
 sub _taxsort {
-  return 0 unless $a cmp $b;
-  return -1 if $b eq 'Tax';
-  return 1 if $a eq 'Tax';
-  return -1 if $b eq 'Other surcharges';
-  return 1 if $a eq 'Other surcharges';
-  $a cmp $b;
+  return 0 unless $a->itemdesc cmp $b->itemdesc;
+  return -1 if $b->itemdesc eq 'Tax';
+  return 1 if $a->itemdesc eq 'Tax';
+  return -1 if $b->itemdesc eq 'Other surcharges';
+  return 1 if $a->itemdesc eq 'Other surcharges';
+  $a->itemdesc cmp $b->itemdesc;
 }
 
 sub _items_tax {
 }
 
 sub _items_tax {
@@ -3831,17 +3988,20 @@ sub _items_cust_bill_pkg {
   my $unsquelched = $opt{unsquelched} || '';
   my $section = $opt{section}->{description} if $opt{section};
   my $summary_page = $opt{summary_page} || '';
   my $unsquelched = $opt{unsquelched} || '';
   my $section = $opt{section}->{description} if $opt{section};
   my $summary_page = $opt{summary_page} || '';
+  my $multilocation = $opt{multilocation} || '';
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
   {
 
 
   my @b = ();
   my ($s, $r, $u) = ( undef, undef, undef );
   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
   {
 
-    foreach ( $s, $r, $u ) {
+    foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
       if ( $_ && !$cust_bill_pkg->hidden ) {
         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+        $_->{amount}      =~ s/^\-0\.00$/0.00/;
         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-        push @b, { %$_ };
+        push @b, { %$_ }
+          unless $_->{amount} == 0;
         $_ = undef;
       }
     }
         $_ = undef;
       }
     }
@@ -3876,10 +4036,18 @@ sub _items_cust_bill_pkg {
           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
 
           my @d = ();
           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
 
           my @d = ();
-          push @d, map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short($self->_date)
-            unless $cust_pkg->part_pkg->hide_svc_detail
-                || $cust_bill_pkg->hidden;
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
+                || $cust_bill_pkg->hidden )
+          {
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short($self->_date);
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($desc, 0, 50). '...'
+                if $format eq 'latex' && length($loc) > 50;
+              push @d, &{$escape_function}($loc);
+            }
+          }
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
           push @d, $cust_bill_pkg->details(%details_opt)
             if $cust_bill_pkg->recur == 0;
 
@@ -3911,8 +4079,8 @@ sub _items_cust_bill_pkg {
                             ? "Usage charges" : $desc;
 
           unless ( $conf->exists('disable_line_item_date_ranges') ) {
                             ? "Usage charges" : $desc;
 
           unless ( $conf->exists('disable_line_item_date_ranges') ) {
-            $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
-                            " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+            $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
+                            " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
           }
 
           my @d = ();
           }
 
           my @d = ();
@@ -3923,14 +4091,23 @@ sub _items_cust_bill_pkg {
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
 
           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
           push @dates, $prev->sdate if $prev;
 
-          push @d, map &{$escape_function}($_),
-                       $cust_pkg->h_labels_short(@dates)
-                                                 #$cust_bill_pkg->edate,
-                                                 #$cust_bill_pkg->sdate)
-            unless $cust_pkg->part_pkg->hide_svc_detail
+          unless ( $cust_pkg->part_pkg->hide_svc_detail
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
                 || $cust_bill_pkg->itemdesc
                 || $cust_bill_pkg->hidden
-                || $is_summary && $type && $type eq 'U';
+                || $is_summary && $type && $type eq 'U' )
+          {
+            push @d, map &{$escape_function}($_),
+                         $cust_pkg->h_labels_short(@dates)
+                                                   #$cust_bill_pkg->edate,
+                                                   #$cust_bill_pkg->sdate)
+            ;
+            if ( $multilocation ) {
+              my $loc = $cust_pkg->location_label;
+              $loc = substr($desc, 0, 50). '...'
+                if $format eq 'latex' && length($loc) > 50;
+              push @d, &{$escape_function}($loc);
+            }
+          }
 
           push @d, $cust_bill_pkg->details(%details_opt)
             unless ($is_summary || $type && $type eq 'R');
 
           push @d, $cust_bill_pkg->details(%details_opt)
             unless ($is_summary || $type && $type eq 'R');
@@ -3995,8 +4172,8 @@ sub _items_cust_bill_pkg {
         if ( $cust_bill_pkg->recur != 0 ) {
           push @b, {
             'description' => "$desc (".
         if ( $cust_bill_pkg->recur != 0 ) {
           push @b, {
             'description' => "$desc (".
-                             time2str("%x", $cust_bill_pkg->sdate). ' - '.
-                             time2str("%x", $cust_bill_pkg->edate). ')',
+                             time2str($date_format, $cust_bill_pkg->sdate). ' - '.
+                             time2str($date_format, $cust_bill_pkg->edate). ')',
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
           };
         }
             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
           };
         }
@@ -4007,11 +4184,13 @@ sub _items_cust_bill_pkg {
 
   }
 
 
   }
 
-  foreach ( $s, $r, $u ) {
-    if ( $_ ) {
+  foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
+    if ( $_  ) {
       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
+      $_->{amount}      =~ s/^\-0\.00$/0.00/;
       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
-      push @b, { %$_ };
+      push @b, { %$_ }
+        unless $_->{amount} == 0;
     }
   }
 
     }
   }
 
@@ -4038,7 +4217,7 @@ sub _items_credits {
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => 'Credit applied '.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       'description' => 'Credit applied '.
-                       time2str("%x",$_->cust_credit->_date). $reason,
+                       time2str($date_format,$_->cust_credit->_date). $reason,
       'amount'      => sprintf("%.2f",$_->amount),
     };
   }
       'amount'      => sprintf("%.2f",$_->amount),
     };
   }
@@ -4058,7 +4237,7 @@ sub _items_payments {
 
     push @b, {
       'description' => "Payment received ".
 
     push @b, {
       'description' => "Payment received ".
-                       time2str("%x",$_->cust_pay->_date ),
+                       time2str($date_format,$_->cust_pay->_date ),
       'amount'      => sprintf("%.2f", $_->amount )
     };
   }
       'amount'      => sprintf("%.2f", $_->amount )
     };
   }
@@ -4175,7 +4354,7 @@ sub re_X {
   my $distinct = '';
   my $orderby = 'ORDER BY cust_bill._date';
 
   my $distinct = '';
   my $orderby = 'ORDER BY cust_bill._date';
 
-  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
+  my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
 
   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
 
   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
@@ -4226,8 +4405,10 @@ Returns an SQL fragment to retreive the amount owed (charged minus credited and
 =cut
 
 sub owed_sql {
 =cut
 
 sub owed_sql {
-  my $class = shift;
-  'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '. 
+    $class->paid_sql($start, $end). ' - '. 
+    $class->credited_sql($start, $end);
 }
 
 =item net_sql
 }
 
 =item net_sql
@@ -4237,8 +4418,8 @@ Returns an SQL fragment to retreive the net amount (charged minus credited).
 =cut
 
 sub net_sql {
 =cut
 
 sub net_sql {
-  my $class = shift;
-  'charged - '. $class->credited_sql;
+  my ($class, $start, $end) = @_;
+  'charged - '. $class->credited_sql($start, $end);
 }
 
 =item paid_sql
 }
 
 =item paid_sql
@@ -4248,9 +4429,13 @@ Returns an SQL fragment to retreive the amount paid against this invoice.
 =cut
 
 sub paid_sql {
 =cut
 
 sub paid_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_bill_pay._date <= $start";
+  $end   &&= "AND cust_bill_pay._date > $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
-       WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
+       WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
 }
 
 =item credited_sql
 }
 
 =item credited_sql
@@ -4260,12 +4445,16 @@ Returns an SQL fragment to retreive the amount credited against this invoice.
 =cut
 
 sub credited_sql {
 =cut
 
 sub credited_sql {
-  #my $class = shift;
+  my ($class, $start, $end) = @_;
+  $start &&= "AND cust_credit_bill._date <= $start";
+  $end   &&= "AND cust_credit_bill._date >  $end";
+  $start = '' unless defined($start);
+  $end   = '' unless defined($end);
   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
-       WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
+       WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
 }
 
 }
 
-=item search_sql HASHREF
+=item search_sql_where HASHREF
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
 
 Class method which returns an SQL WHERE fragment to search for parameters
 specified in HASHREF.  Valid parameters are
@@ -4308,10 +4497,10 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
 
 =cut
 
 
 =cut
 
-sub search_sql {
+sub search_sql_where {
   my($class, $param) = @_;
   if ( $DEBUG ) {
   my($class, $param) = @_;
   if ( $DEBUG ) {
-    warn "$me search_sql called with params: \n".
+    warn "$me search_sql_where called with params: \n".
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }
 
          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
   }