package categories (meta package classes) and grouping invoices by them
[freeside.git] / FS / FS / cust_bill.pm
index ee95be8..4618958 100644 (file)
@@ -12,7 +12,7 @@ use String::ShellQuote;
 use HTML::Entities;
 use Locale::Country;
 use FS::UID qw( datasrc );
-use FS::Misc qw( send_email send_fax generate_ps do_print );
+use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
 use FS::Record qw( qsearch qsearchs dbh );
 use FS::cust_main_Mixin;
 use FS::cust_main;
@@ -798,6 +798,9 @@ single agent) or an arrayref of agentnums.
 
 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
+invoice and all older invoices is greater than the specified amount.
+
 =cut
 
 sub queueable_send {
@@ -828,15 +831,22 @@ sub send {
       ? shift
       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
 
+  my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
+
+  return ''
+    unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
+
   my @invoicing_list = $self->cust_main->invoicing_list;
 
+  #$self->email_invoice($template, $invoice_from)
   $self->email($template, $invoice_from)
     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
+  #$self->print_invoice($template)
   $self->print($template)
     if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-  $self->fax($template)
+  $self->fax_invoice($template)
     if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
   '';
@@ -868,6 +878,7 @@ sub queueable_email {
 
 }
 
+#sub email_invoice {
 sub email {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
@@ -917,6 +928,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
+#sub print_invoice {
 sub print {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
@@ -924,7 +936,7 @@ sub print {
   do_print $self->lpr_data($template);
 }
 
-=item fax [ TEMPLATENAME ] 
+=item fax_invoice [ TEMPLATENAME ] 
 
 Faxes this invoice.
 
@@ -932,7 +944,7 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
 =cut
 
-sub fax {
+sub fax_invoice {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
 
@@ -1347,7 +1359,7 @@ sub print_csv {
       if ( $cust_bill_pkg->pkgnum ) {
       
         ($pkg, $setup, $recur, $sdate, $edate) = (
-          $cust_bill_pkg->cust_pkg->part_pkg->pkg,
+          $cust_bill_pkg->part_pkg->pkg,
           ( $cust_bill_pkg->setup != 0
             ? sprintf("%.2f", $cust_bill_pkg->setup )
             : '' ),
@@ -1468,7 +1480,7 @@ sub realtime_bop {
              $cust_main->agentnum. ")";
     my $agent = $agent_obj->agent;
     my $pkgs = join(', ',
-      map { $_->cust_pkg->part_pkg->pkg }
+      map { $_->part_pkg->pkg }
         grep { $_->pkgnum } $self->cust_bill_pkg
     );
     $description = eval qq("$dtempl");
@@ -1635,7 +1647,7 @@ sub print_generic {
   $templatefile .= "_$template"
     if length($template);
   my @invoice_template = map "$_\n", $conf->config($templatefile)
-    or die "cannot load config file $templatefile";
+    or die "cannot load config data $templatefile";
 
   my $old_latex = '';
   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
@@ -1653,7 +1665,7 @@ sub print_generic {
   );
 
   $text_template->compile()
-    or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
+    or die "Can't compile $templatefile: $Text::Template::ERROR\n";
 
 
   # additional substitution could possibly cause breakage in existing templates
@@ -1663,6 +1675,7 @@ sub print_generic {
                  'footer'        => sub { map "$_", @_ },
                  'smallfooter'   => sub { map "$_", @_ },
                  'returnaddress' => sub { map "$_", @_ },
+                 'coupon'        => sub { map "$_", @_ },
                },
     'html'  => {
                  'notes' =>
@@ -1674,8 +1687,10 @@ sub print_generic {
                        s/\\item /  <li>/g;
                        s/\\end\{enumerate\}/<\/ol>/g;
                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
-                       s/\\\\\*/ /;
+                       s/\\\\\*/<br>/g;
                        s/\\dollar ?/\$/g;
+                       s/\\#/#/g;
+                       s/~/&nbsp;/g;
                        $_;
                      }  @_
                    },
@@ -1692,6 +1707,7 @@ sub print_generic {
                        $_;
                      }  @_
                    },
+                 'coupon'        => sub { "" },
                },
     'template' => {
                  'notes' =>
@@ -1721,6 +1737,7 @@ sub print_generic {
                        $_;
                      }  @_
                    },
+                 'coupon'        => sub { "" },
                },
   );
 
@@ -1738,7 +1755,7 @@ sub print_generic {
                          );
   my $escape_function = $escape_functions{$format};
 
-  my %date_formats = ( 'latex'    => '%b, %o, %Y',
+  my %date_formats = ( 'latex'    => '%b %o, %Y',
                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
                        'template' => '%s',
                      );
@@ -1783,13 +1800,17 @@ sub print_generic {
           );
   } elsif ( grep /\S/, $conf->config('company_address') ) {
 
-    $returnaddress = join( "\n", $conf->config('company_address') );
-
-    $returnaddress =
-      join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
-                            $conf->config('company_address')
-          )
-        if $format eq 'latex';
+    my $convert_map = $convert_maps{$format}{'returnaddress'};
+    $returnaddress = join( "\n", &$convert_map(
+                                   map { s/( {2,})/'~' x length($1)/eg;
+                                         s/$/\\\\\*/;
+                                         $_
+                                       }
+                                     ( $conf->config('company_name'),
+                                       $conf->config('company_address'),
+                                     )
+                                 )
+                     );
 
   } else {
 
@@ -1809,6 +1830,7 @@ sub print_generic {
     '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),
@@ -1817,7 +1839,7 @@ sub print_generic {
     'state'           => &$escape_function($cust_main->state),
     'zip'             => &$escape_function($cust_main->zip),
     'returnaddress'   => $returnaddress,
-    'quantity'        => 1,
+    #'quantity'        => 1,
     'terms'           => $self->terms,
     'template'        => $params{'template'},
     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
@@ -1825,8 +1847,16 @@ sub print_generic {
     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
     'page'            => 1,
     'total_pages'     => 1,
+    'ship_enable'     => $conf->exists('invoice-ship_address'),
+    'unitprices'      => $conf->exists('invoice-unitprice'),
   );
 
+  my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
+  foreach ( qw( contact company address1 address2 city state zip country fax) ){
+    my $method = $prefix.$_;
+    $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
+  }
+  
   $invoice_data{'cid'} = $params{'cid'}
     if $params{'cid'};
 
@@ -1858,36 +1888,50 @@ sub print_generic {
   push @address, ''
     while (scalar(@address) < 5);
 
+  $invoice_data{'logo_file'} = $params{'logo_file'}
+    if $params{'logo_file'};
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+#  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{'balance'} = $balance_due;
+
   #do variable substitution in notes, footer, smallfooter
-  foreach my $include (qw( notes footer smallfooter )) {
+  foreach my $include (qw( notes footer smallfooter coupon )) {
+
+    my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
+    my @inc_src;
+
+    if ( $conf->exists($inc_file) && length( $conf->config($inc_file) ) ) {
 
-    my @inc_src = $conf->config_orbase("invoice_latex$include", $template );
-    my $convert_map = $convert_maps{$format}{$include};
+      @inc_src = $conf->config($inc_file);
 
-    if (
-           defined( $conf->config_orbase("invoice_${format}$include", $template) )
-        && length(  $conf->config_orbase('invoice_${format}$include', $template) )
-    ) {
-      @inc_src = $conf->config_orbase("invoice_${format}$include", $template );
     } else {
-      @inc_src =
-        map { s/\[@--/$delimiters{$format}[0]/g;
-              s/--@]/$delimiters{$format}[1]/g;
-              $_;
-            } 
-        &$convert_map(
-                       $conf->config_orbase("invoice_latex$include", $template )
-                     );
+
+      $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) );
+
     }
 
     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";
+    ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
 
-    $inc_tt->compile()
-      or die "can't compile template: $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 );
 
@@ -1900,11 +1944,6 @@ sub print_generic {
       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
       : $nbsp;
 
-  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
-#  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;
-
   my %money_chars = ( 'latex'    => '',
                       'html'     => $conf->config('money_char') || '$',
                       'template' => '',
@@ -1932,6 +1971,14 @@ sub print_generic {
                                             sprintf('%.2f', $pr_total),
                          };
 
+  my $taxtotal = 0;
+  my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
+                      'subtotal'    => $taxtotal }; # adjusted below
+
+  my $adjusttotal = 0;
+  my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
+                         'subtotal'    => 0 }; # adjusted below
+
   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
   if ( $multisection ) {
     push @sections, $self->_items_sections;
@@ -1939,7 +1986,11 @@ sub print_generic {
     push @sections, { 'description' => '', 'subtotal' => '' };
   }
 
-  foreach my $line_item ( $self->_items_previous ) {
+  foreach my $line_item ( $conf->exists('disable_previous_balance') 
+                            ? ()
+                            : $self->_items_previous
+                        )
+  {
     my $detail = {
       ext_description => [],
     };
@@ -1952,10 +2003,8 @@ sub print_generic {
         &$escape_function($_);
       } @{$line_item->{'ext_description'}};
     }
-    {
-      my $money = $old_latex ? '' : $money_char;
-      $detail->{'amount'} = $money. $line_item->{'amount'};
-    }
+    $detail->{'amount'} = ( $old_latex ? '' : $money_char).
+                          $line_item->{'amount'};
     $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
   
     push @detail_items, $detail;
@@ -1964,7 +2013,7 @@ sub print_generic {
                ];
   }
   
-  if (@pr_cust_bill) {
+  if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
     push @buf, ['','-----------'];
     push @buf, [ 'Total Previous Balance',
                  $money_char. sprintf("%10.2f", $pr_total) ];
@@ -1985,24 +2034,24 @@ sub print_generic {
 
     my %options = ();
     $options{'section'} = $section if $multisection;
+    $options{'format'} = $format;
+    $options{'escape_function'} = $escape_function;
 
     foreach my $line_item ( $self->_items_pkg(%options) ) {
       my $detail = {
         ext_description => [],
       };
       $detail->{'ref'} = $line_item->{'pkgnum'};
-      $detail->{'quantity'} = 1;
+      $detail->{'quantity'} = $line_item->{'quantity'};
       $detail->{'section'} = $section;
       $detail->{'description'} = &$escape_function($line_item->{'description'});
       if ( exists $line_item->{'ext_description'} ) {
-        @{$detail->{'ext_description'}} = map {
-          &$escape_function($_);
-        } @{$line_item->{'ext_description'}};
-      }
-      {
-        my $money = $old_latex ? '' : $money_char;
-        $detail->{'amount'} = $money. $line_item->{'amount'};
+        @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
       }
+      $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
+                              $line_item->{'amount'};
+      $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
+                                 $line_item->{'unit_amount'};
       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
   
       push @detail_items, $detail;
@@ -2025,17 +2074,29 @@ sub print_generic {
   
   }
   
-  if ( $multisection ) {
-    unshift @sections, $previous_section;
+  if ( $multisection && !$conf->exists('disable_previous_balance') ) {
+    unshift @sections, $previous_section if $pr_total;
   }
 
-  my $taxtotal = 0;
   foreach my $tax ( $self->_items_tax ) {
     my $total = {};
     $total->{'total_item'} = &$escape_function($tax->{'description'});
     $taxtotal += $tax->{'amount'};
     $total->{'total_amount'} = $other_money_char. $tax->{'amount'};
-    push @total_items, $total;
+    if ( $multisection ) {
+      my $money = $old_latex ? '' : $money_char;
+      push @detail_items, {
+        ext_description => [],
+        ref          => '',
+        quantity     => '',
+        description  => &$escape_function($tax->{'description'}),
+        amount       => $money. $tax->{'amount'},
+        product_code => '',
+        section      => $tax_section,
+      };
+    }else{
+      push @total_items, $total;
+    }
     push @buf,[ $total->{'total_item'},
                 $money_char. sprintf("%10.2f", $total->{'total_amount'}),
               ];
@@ -2044,18 +2105,26 @@ sub print_generic {
   
   if ( $taxtotal ) {
     my $total = {};
+    $total->{'total_item'} = 'Sub-total';
+    $total->{'total_amount'} =
+      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
+
     if ( $multisection ) {
-      $total->{'total_item'} = 'New charges sub-total';
+      $tax_section->{'subtotal'} = $other_money_char.
+                                   sprintf('%.2f', $taxtotal);
+      $tax_section->{'pretotal'} = 'New charges sub-total '.
+                                   $total->{'total_amount'};
+      push @sections, $tax_section if $taxtotal;
     }else{
-      $total->{'total_item'} = 'Sub-total';
+      unshift @total_items, $total;
     }
-    $total->{'total_amount'} =
-      $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
-    unshift @total_items, $total;
   }
   
   push @buf,['','-----------'];
-  push @buf,['Total New Charges',
+  push @buf,[( $conf->exists('disable_previous_balance') 
+               ? 'Total Charges'
+               : 'Total New Charges'
+             ),
              $money_char. sprintf("%10.2f",$self->charged) ];
   push @buf,['',''];
 
@@ -2063,70 +2132,124 @@ sub print_generic {
     my $total = {};
     $total->{'total_item'} = &$embolden_function('Total');
     $total->{'total_amount'} =
-    $total->{'total_amount'} =
       &$embolden_function(
-        $other_money_char.  sprintf('%.2f', $self->charged + $pr_total )
+        $other_money_char.
+        sprintf( '%.2f',
+                 $self->charged + ( $conf->exists('disable_previous_balance')
+                                    ? 0
+                                    : $pr_total
+                                  )
+               )
       );
-    push @total_items, $total;
+    if ( $multisection ) {
+      $adjust_section->{'pretotal'} = 'New charges total '.
+                                      $total->{'total_amount'};
+    }else{
+      push @total_items, $total;
+    }
     push @buf,['','-----------'];
     push @buf,['Total Charges',
-               $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
+               $money_char.
+               sprintf( '%10.2f', $self->charged +
+                                    ( $conf->exists('disable_previous_balance')
+                                        ? 0
+                                        : $pr_total
+                                    )
+                      )
+              ];
     push @buf,['',''];
   }
   
-
-  #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+  unless ( $conf->exists('disable_previous_balance') ) {
+    #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
   
-  # credits
-  foreach my $credit ( $self->_items_credits ) {
-    my $total;
-    $total->{'total_item'} = &$escape_function($credit->{'description'});
-    #$credittotal
-    $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
-    push @total_items, $total;
-  }
+    # credits
+    foreach my $credit ( $self->_items_credits ) {
+      my $total;
+      $total->{'total_item'} = &$escape_function($credit->{'description'});
+      #$credittotal
+      $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
+      $adjusttotal += $credit->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($credit->{'description'}),
+          amount       => $money. $credit->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      }else{
+        push @total_items, $total;
+      }
+    }
   
-  # credits (again)
-  foreach ( $self->cust_credited ) {
-
-    #something more elaborate if $_->amount ne $_->cust_credit->credited ?
-
-    my $reason = substr($_->cust_credit->reason,0,32);
-    $reason .= '...' if length($reason) < length($_->cust_credit->reason);
-    $reason = " ($reason) " if $reason;
-    push @buf,[
-      "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
-      $money_char. sprintf("%10.2f",$_->amount)
-    ];
-  }
+    # credits (again)
+    foreach ( $self->cust_credited ) {
+  
+      #something more elaborate if $_->amount ne $_->cust_credit->credited ?
+
+      my $reason = substr($_->cust_credit->reason,0,32);
+      $reason .= '...' if length($reason) < length($_->cust_credit->reason);
+      $reason = " ($reason) " if $reason;
+      push @buf,[
+        "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".        $reason,
+        $money_char. sprintf("%10.2f",$_->amount)
+      ];
+    }
 
-  # payments
-  foreach my $payment ( $self->_items_payments ) {
-    my $total = {};
-    $total->{'total_item'} = &$escape_function($payment->{'description'});
-    #$paymenttotal
-    $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
-    push @total_items, $total;
-    push @buf, [ $payment->{'description'},
-                 $money_char. sprintf("%10.2f", $payment->{'amount'}),
-               ];
-  }
+    # payments
+    foreach my $payment ( $self->_items_payments ) {
+      my $total = {};
+      $total->{'total_item'} = &$escape_function($payment->{'description'});
+      #$paymenttotal
+      $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
+      $adjusttotal += $payment->{'amount'};
+      if ( $multisection ) {
+        my $money = $old_latex ? '' : $money_char;
+        push @detail_items, {
+          ext_description => [],
+          ref          => '',
+          quantity     => '',
+          description  => &$escape_function($payment->{'description'}),
+          amount       => $money. $payment->{'amount'},
+          product_code => '',
+          section      => $adjust_section,
+        };
+      }else{
+        push @total_items, $total;
+      }
+      push @buf, [ $payment->{'description'},
+                   $money_char. sprintf("%10.2f", $payment->{'amount'}),
+                 ];
+    }
   
-  { 
-    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 )
-      );
-    push @total_items, $total;
-    push @buf,['','-----------'];
-    push @buf,[$self->balance_due_msg, $money_char. 
-      sprintf("%10.2f", $balance_due ) ];
-  }
+    if ( $multisection ) {
+      $adjust_section->{'subtotal'} = $other_money_char.
+                                      sprintf('%.2f', $adjusttotal);
+      push @sections, $adjust_section;
+    }
 
-  $invoice_data{'logo_file'} = $params{'logo_file'}
-    if $params{'logo_file'};
+    { 
+      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 )
+        );
+      if ( $multisection ) {
+        $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
+                                         $total->{'total_amount'};
+      }else{
+        push @total_items, $total;
+      }
+      push @buf,['','-----------'];
+      push @buf,[$self->balance_due_msg, $money_char. 
+        sprintf("%10.2f", $balance_due ) ];
+    }
+  }
 
   $invoice_lines = 0;
   my $wasfunc = 0;
@@ -2195,8 +2318,8 @@ sub print_ps {
   my ($file, $lfile) = $self->print_latex(@_);
   my $ps = generate_ps($file);
   unlink($lfile);
-  $ps;
 
+  $ps;
 }
 
 =item print_pdf [ TIME [ , TEMPLATE ] ]
@@ -2214,44 +2337,10 @@ sub print_pdf {
   my $self = shift;
 
   my ($file, $lfile) = $self->print_latex(@_);
+  my $pdf = generate_pdf($file);
+  unlink($lfile);
 
-  my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
-  chdir($dir);
-
-  #system('pdflatex', "$file.tex");
-  #system('pdflatex', "$file.tex");
-  #! LaTeX Error: Unknown graphics extension: .eps.
-
-  my $sfile = shell_quote $file;
-
-  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed; see $file.log for details?\n";
-  system("pslatex $sfile.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed; see $file.log for details?\n";
-
-  #system('dvipdf', "$file.dvi", "$file.pdf" );
-  system(
-    "dvips -q -t letter -f $sfile.dvi ".
-    "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$sfile.pdf ".
-    "     -c save pop -"
-  ) == 0
-    or die "dvips | gs failed: $!";
-
-  open(PDF, "<$file.pdf")
-    or die "can't open $file.pdf: $! (error in LaTeX template?)\n";
-
-  unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
-  unlink("$lfile");
-
-  my $pdf = '';
-  while (<PDF>) {
-    $pdf .= $_;
-  }
-
-  close PDF;
-
-  return $pdf;
-
+  $pdf;
 }
 
 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
@@ -2397,6 +2486,18 @@ sub balance_due_msg {
   $msg;
 }
 
+=item invnum_date_pretty
+
+Returns a string with the invoice number and date, for example:
+"Invoice #54 (3/20/2008)"
+
+=cut
+
+sub invnum_date_pretty {
+  my $self = shift;
+  'Invoice #'. $self->invnum. ' ('. time2str('%x', $self->_date). ')';
+}
+
 sub _items_sections {
   my $self = shift;
 
@@ -2405,7 +2506,7 @@ sub _items_sections {
 
     if ( $cust_bill_pkg->pkgnum > 0 ) {
 
-      my $desc = $cust_bill_pkg->cust_pkg->part_pkg->classname;
+      my $desc = $cust_bill_pkg->part_pkg->categoryname;
 
       $s{$desc} += $cust_bill_pkg->setup
         if ( $cust_bill_pkg->setup != 0 );
@@ -2423,11 +2524,14 @@ sub _items_sections {
 
 sub _items {
   my $self = shift;
-  my @display = scalar(@_)
-                ? @_
-                : qw( _items_previous _items_pkg );
-                #: qw( _items_pkg );
-                #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+
+  #my @display = scalar(@_)
+  #              ? @_
+  #              : qw( _items_previous _items_pkg );
+  #              #: qw( _items_pkg );
+  #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
+  my @display = qw( _items_previous _items_pkg );
+
   my @b = ();
   foreach my $display ( @display ) {
     push @b, $self->$display(@_);
@@ -2472,61 +2576,96 @@ sub _items_pkg {
   my @cust_bill_pkg =
     grep { $_->pkgnum &&
            ( defined($section)
-               ? $_->cust_pkg->part_pkg->classname eq $section->{'description'}
+               ? $_->part_pkg->categoryname eq $section->{'description'}
                : 1
            )
          } $self->cust_bill_pkg;
   $self->_items_cust_bill_pkg(\@cust_bill_pkg, %options);
 }
 
+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;
+}
+
 sub _items_tax {
   my $self = shift;
-  my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
+  my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
 }
 
 sub _items_cust_bill_pkg {
   my $self = shift;
   my $cust_bill_pkg = shift;
+  my %opt = @_;
+
+  my $format = $opt{format} || '';
+  my $escape_function = $opt{escape_function} || sub { shift };
 
   my @b = ();
   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
 
+    my $cust_pkg = $cust_bill_pkg->cust_pkg;
+
     my $desc = $cust_bill_pkg->desc;
 
+    my %details_opt = ( 'format'          => $format,
+                        'escape_function' => $escape_function,
+                      );
+
     if ( $cust_bill_pkg->pkgnum > 0 ) {
 
       if ( $cust_bill_pkg->setup != 0 ) {
+
         my $description = $desc;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = $cust_bill_pkg->cust_pkg->h_labels_short($self->_date);
-        push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
+
+        my @d = map &{$escape_function}($_),
+                       $cust_pkg->h_labels_short($self->_date);
+        push @d, $cust_bill_pkg->details(%details_opt)
+          if $cust_bill_pkg->recur == 0;
+
         push @b, {
           description     => $description,
           #pkgpart         => $part_pkg->pkgpart,
           pkgnum          => $cust_bill_pkg->pkgnum,
           amount          => sprintf("%.2f", $cust_bill_pkg->setup),
+          unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitsetup),
+          quantity        => $cust_bill_pkg->quantity,
           ext_description => \@d,
         };
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
+
+        my $description = $desc;
+        unless ( $conf->exists('disable_line_item_date_ranges') ) {
+          $desc .= " (" . time2str("%x", $cust_bill_pkg->sdate).
+                   " - ". time2str("%x", $cust_bill_pkg->edate). ")";
+        }
+
+        #at least until cust_bill_pkg has "past" ranges in addition to
+        #the "future" sdate/edate ones... see #3032
+        my @d = map &{$escape_function}($_),
+                    $cust_pkg->h_labels_short($self->_date);
+                                              #$cust_bill_pkg->edate,
+                                              #$cust_bill_pkg->sdate),
+        push @d, $cust_bill_pkg->details(%details_opt);
+
         push @b, {
-          description     => $desc .
-                             ( $conf->exists('disable_line_item_date_ranges')
-                               ? ''
-                               : " (" .time2str("%x", $cust_bill_pkg->sdate).
-                                 " - ".time2str("%x", $cust_bill_pkg->edate).")"
-                             ),
+          description     => $description,
           #pkgpart         => $part_pkg->pkgpart,
           pkgnum          => $cust_bill_pkg->pkgnum,
           amount          => sprintf("%.2f", $cust_bill_pkg->recur),
-          ext_description =>
-            [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate,
-                                                        $cust_bill_pkg->sdate),
-              $cust_bill_pkg->details,
-            ],
+          unit_amount     => sprintf("%.2f", $cust_bill_pkg->unitrecur),
+          quantity        => $cust_bill_pkg->quantity,
+          ext_description => \@d,
         };
+
       }
 
     } else { #pkgnum tax or one-shot line item (??)
@@ -2643,7 +2782,7 @@ use Data::Dumper;
 use MIME::Base64;
 sub process_re_X {
   my( $method, $job ) = ( shift, shift );
-  warn "process_re_X $method for job $job\n" if $DEBUG;
+  warn "$me process_re_X $method for job $job\n" if $DEBUG;
 
   my $param = thaw(decode_base64(shift));
   warn Dumper($param) if $DEBUG;
@@ -2669,16 +2808,20 @@ sub re_X {
 
   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
 
-  my $addl_from = 'left join cust_main using ( custnum )';
+  my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
      
-  my @cust_bill = qsearch( 'cust_bill',
-                           {},
-                           #"$distinct cust_bill.*",
-                           "cust_bill.*",
-                           $extra_sql,
-                           '',
-                           $addl_from
-                         );
+  my @cust_bill = qsearch( {
+    #'select'    => "cust_bill.*",
+    'table'     => 'cust_bill',
+    'addl_from' => $addl_from,
+    'hashref'   => {},
+    'extra_sql' => $extra_sql,
+    'order_by'  => $orderby,
+    'debug' => 1,
+  } );
+
+  warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
+    if $DEBUG;
 
   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
   foreach my $cust_bill ( @cust_bill ) {
@@ -2788,6 +2931,11 @@ Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
 
 sub search_sql {
   my($class, $param) = @_;
+  if ( $DEBUG ) {
+    warn "$me search_sql called with params: \n".
+         join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
+  }
+
   my @search = ();
 
   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
@@ -2838,7 +2986,22 @@ sub search_sql {
 
   }
 
-  push @search, $FS::CurrentUser::CurrentUser->agentnums_sql;
+  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 );