oops, losing notes!
[freeside.git] / FS / FS / cust_bill.pm
index 3b76dd2..2c25c6f 100644 (file)
@@ -1,14 +1,17 @@
 package FS::cust_bill;
 
 use strict;
 package FS::cust_bill;
 
 use strict;
-use vars qw( @ISA $conf $money_char );
+use vars qw( @ISA $DEBUG $conf $money_char );
 use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
 use vars qw( $invoice_lines @buf ); #yuck
 use Date::Format;
-use Text::Template;
+use Text::Template 1.20;
 use File::Temp 0.14;
 use File::Temp 0.14;
+use String::ShellQuote;
+use HTML::Entities;
+use Locale::Country;
 use FS::UID qw( datasrc );
 use FS::Record qw( qsearch qsearchs );
 use FS::UID qw( datasrc );
 use FS::Record qw( qsearch qsearchs );
-use FS::Misc qw( send_email );
+use FS::Misc qw( send_email send_fax );
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
 use FS::cust_main;
 use FS::cust_bill_pkg;
 use FS::cust_credit;
@@ -20,6 +23,8 @@ use FS::cust_bill_event;
 
 @ISA = qw( FS::Record );
 
 
 @ISA = qw( FS::Record );
 
+$DEBUG = 0;
+
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
 #ask FS::UID to run this stuff for us later
 FS::UID->install_callback( sub { 
   $conf = new FS::Conf;
@@ -317,7 +322,224 @@ sub owed {
   $balance;
 }
 
   $balance;
 }
 
-=item send [ TEMPLATENAME [ , AGENTNUM ] ]
+
+=item generate_email PARAMHASH
+
+PARAMHASH can contain the following:
+
+=over 4
+
+=item from       => sender address, required
+
+=item tempate    => alternate template name, optional
+
+=item print_text => text attachment arrayref, optional
+
+=item subject    => email subject, optional
+
+=back
+
+Returns an argument list to be passed to L<FS::Misc::send_email>.
+
+=cut
+
+use MIME::Entity;
+
+sub generate_email {
+
+  my $self = shift;
+  my %args = @_;
+
+  my $me = '[FS::cust_bill::generate_email]';
+
+  my %return = (
+    'from'      => $args{'from'},
+    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+  );
+
+  if (ref($args{'to'} eq 'ARRAY')) {
+    $return{'to'} = $args{'to'};
+  } else {
+    $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
+                           $self->cust_main->invoicing_list
+                    ];
+  }
+
+  if ( $conf->exists('invoice_html') ) {
+
+    warn "$me creating HTML/text multipart message"
+      if $DEBUG;
+
+    $return{'nobody'} = 1;
+
+    my $alternative = build MIME::Entity
+      'Type'        => 'multipart/alternative',
+      'Encoding'    => '7bit',
+      'Disposition' => 'inline'
+    ;
+
+    my $data;
+    if ( $conf->exists('invoice_email_pdf')
+         and scalar($conf->config('invoice_email_pdf_note')) ) {
+
+      warn "$me using 'invoice_email_pdf_note' in multipart message"
+        if $DEBUG;
+      $data = [ map { $_ . "\n" }
+                    $conf->config('invoice_email_pdf_note')
+              ];
+
+    } else {
+
+      warn "$me not using 'invoice_email_pdf_note' in multipart message"
+        if $DEBUG;
+      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
+        $data = $args{'print_text'};
+      } else {
+        $data = [ $self->print_text('', $args{'template'}) ];
+      }
+
+    }
+
+    $alternative->attach(
+      'Type'        => 'text/plain',
+      #'Encoding'    => 'quoted-printable',
+      'Encoding'    => '7bit',
+      'Data'        => $data,
+      'Disposition' => 'inline',
+    );
+
+    $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
+    my $content_id = join('.', rand()*(2**32), $$, time). "\@$1";
+
+    my $image = build MIME::Entity
+      'Type'       => 'image/png',
+      'Encoding'   => 'base64',
+      'Path'       => "$FS::UID::conf_dir/conf.$FS::UID::datasrc/logo.png",
+      'Filename'   => 'logo.png',
+      'Content-ID' => "<$content_id>",
+    ;
+
+    $alternative->attach(
+      'Type'        => 'text/html',
+      'Encoding'    => 'quoted-printable',
+      'Data'        => [ '<html>',
+                         '  <head>',
+                         '    <title>',
+                         '      '. encode_entities($return{'subject'}), 
+                         '    </title>',
+                         '  </head>',
+                         '  <body bgcolor="#e8e8e8">',
+                         $self->print_html('', $args{'template'}, $content_id),
+                         '  </body>',
+                         '</html>',
+                       ],
+      'Disposition' => 'inline',
+      #'Filename'    => 'invoice.pdf',
+    );
+
+    if ( $conf->exists('invoice_email_pdf') ) {
+
+      #attaching pdf too:
+      # multipart/mixed
+      #   multipart/related
+      #     multipart/alternative
+      #       text/plain
+      #       text/html
+      #     image/png
+      #   application/pdf
+
+      my $related = build MIME::Entity 'Type'     => 'multipart/related',
+                                       'Encoding' => '7bit';
+
+      #false laziness w/Misc::send_email
+      $related->head->replace('Content-type',
+        $related->mime_type.
+        '; boundary="'. $related->head->multipart_boundary. '"'.
+        '; type=multipart/alternative'
+      );
+
+      $related->add_part($alternative);
+
+      $related->add_part($image);
+
+      my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'});
+
+      $return{'mimeparts'} = [ $related, $pdf ];
+
+    } else {
+
+      #no other attachment:
+      # multipart/related
+      #   multipart/alternative
+      #     text/plain
+      #     text/html
+      #   image/png
+
+      $return{'content-type'} = 'multipart/related';
+      $return{'mimeparts'} = [ $alternative, $image ];
+      $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
+      #$return{'disposition'} = 'inline';
+
+    }
+  
+  } else {
+
+    if ( $conf->exists('invoice_email_pdf') ) {
+      warn "$me creating PDF attachment"
+        if $DEBUG;
+
+      #mime parts arguments a la MIME::Entity->build().
+      $return{'mimeparts'} = [
+        { $self->mimebuild_pdf('', $args{'template'}) }
+      ];
+    }
+  
+    if ( $conf->exists('invoice_email_pdf')
+         and scalar($conf->config('invoice_email_pdf_note')) ) {
+
+      warn "$me using 'invoice_email_pdf_note'"
+        if $DEBUG;
+      $return{'body'} = [ map { $_ . "\n" }
+                              $conf->config('invoice_email_pdf_note')
+                        ];
+
+    } else {
+
+      warn "$me not using 'invoice_email_pdf_note'"
+        if $DEBUG;
+      if ( ref($args{'print_text'}) eq 'ARRAY' ) {
+        $return{'body'} = $args{'print_text'};
+      } else {
+        $return{'body'} = [ $self->print_text('', $args{'template'}) ];
+      }
+
+    }
+
+  }
+
+  %return;
+
+}
+
+=item mimebuild_pdf
+
+Returns a list suitable for passing to MIME::Entity->build(), representing
+this invoice as PDF attachment.
+
+=cut
+
+sub mimebuild_pdf {
+  my $self = shift;
+  (
+    'Type'        => 'application/pdf',
+    'Encoding'    => 'base64',
+    'Data'        => [ $self->print_pdf(@_) ],
+    'Disposition' => 'attachment',
+    'Filename'    => 'invoice.pdf',
+  );
+}
+
+=item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
 
 Sends this invoice to the destinations configured for this customer: send
 emails or print.  See L<FS::cust_main_invoice>.
 
 Sends this invoice to the destinations configured for this customer: send
 emails or print.  See L<FS::cust_main_invoice>.
@@ -327,49 +549,97 @@ TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 AGENTNUM, if specified, means that this invoice will only be sent for customers
 of the specified agent.
 
 AGENTNUM, if specified, means that this invoice will only be sent for customers
 of the specified agent.
 
+INVOICE_FROM, if specified, overrides the default email invoice From: address.
+
 =cut
 
 sub send {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
 =cut
 
 sub send {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
-  return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift;
+  return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
+  my $invoice_from =
+    scalar(@_)
+      ? shift
+      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
 
 
-  my @print_text = $self->print_text('', $template);
+  #my @print_text = $self->print_text('', $template);
   my @invoicing_list = $self->cust_main->invoicing_list;
 
   my @invoicing_list = $self->cust_main->invoicing_list;
 
-  if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list  ) { #email
+  if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list  ) {
+    #email
 
     #better to notify this person than silence
 
     #better to notify this person than silence
-    @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list;
+    @invoicing_list = ($invoice_from) unless @invoicing_list;
 
     my $error = send_email(
 
     my $error = send_email(
-      'from'    => $conf->config('invoice_from'),
-      'to'      => [ grep { $_ ne 'POST' } @invoicing_list ],
-      'subject' => 'Invoice',
-      'body'    => \@print_text,
+      $self->generate_email(
+        'from'       => $invoice_from,
+        'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+        #'print_text' => [ @print_text ],
+        'template'   => $template,
+      )
     );
     die "can't email invoice: $error\n" if $error;
     );
     die "can't email invoice: $error\n" if $error;
+    #die "$error\n" if $error;
 
   }
 
 
   }
 
-  if ( $conf->config('invoice_latex') ) {
-    @print_text = $self->print_ps('', $template);
-  }
+  if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
+    my $lpr_data;
+    if ($conf->config('invoice_latex')) {
+      $lpr_data = [ $self->print_ps('', $template) ];
+    } else {
+      $lpr_data = [ $self->print_text('', $template) ];
+    }
+
+    if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
+      my $lpr = $conf->config('lpr');
+      open(LPR, "|$lpr")
+        or die "Can't open pipe to $lpr: $!\n";
+      print LPR @{$lpr_data};
+      close LPR
+        or die $! ? "Error closing $lpr: $!\n"
+                  : "Exit status $? from $lpr\n";
+    }
+
+    if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax
+      die 'FAX invoice destination not supported with plain text invoices.'
+        unless $conf->exists('invoice_latex');
+      my $dialstring = $self->cust_main->getfield('fax');
+      #Check $dialstring?
+      my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring);
+      die $error if $error;
+    }
 
 
-  if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
-    my $lpr = $conf->config('lpr');
-    open(LPR, "|$lpr")
-      or die "Can't open pipe to $lpr: $!\n";
-    print LPR @print_text;
-    close LPR
-      or die $! ? "Error closing $lpr: $!\n"
-                : "Exit status $? from $lpr\n";
   }
 
   '';
 
 }
 
   }
 
   '';
 
 }
 
+=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
+
+Like B<send>, but only sends the invoice if it is the newest open invoice for
+this customer.
+
+=cut
+
+sub send_if_newest {
+  my $self = shift;
+
+  return ''
+    if scalar(
+               grep { $_->owed > 0 } 
+                    qsearch('cust_bill', {
+                      'custnum' => $self->custnum,
+                      #'_date'   => { op=>'>', value=>$self->_date },
+                      'invnum'  => { op=>'>', value=>$self->invnum },
+                    } )
+             );
+    
+  $self->send(@_);
+}
+
 =item send_csv OPTIONS
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
 =item send_csv OPTIONS
 
 Sends invoice as a CSV data-file to a remote host with the specified protocol.
@@ -652,7 +922,7 @@ sub batch_card {
     'state'    => $cust_main->getfield('state'),
     'zip'      => $cust_main->getfield('zip'),
     'country'  => $cust_main->getfield('country'),
     'state'    => $cust_main->getfield('state'),
     'zip'      => $cust_main->getfield('zip'),
     'country'  => $cust_main->getfield('country'),
-    'cardnum'  => $cust_main->getfield('payinfo'),
+    'cardnum'  => $cust_main->payinfo,
     'exp'      => $cust_main->getfield('paydate'),
     'payname'  => $cust_main->getfield('payname'),
     'amount'   => $self->owed,
     'exp'      => $cust_main->getfield('paydate'),
     'payname'  => $cust_main->getfield('payname'),
     'amount'   => $self->owed,
@@ -665,24 +935,38 @@ sub batch_card {
 
 sub _agent_template {
   my $self = shift;
 
 sub _agent_template {
   my $self = shift;
+  $self->_agent_plandata('agent_templatename');
+}
 
 
-  my $cust_bill_event = qsearchs( 'part_bill_event',
+sub _agent_invoice_from {
+  my $self = shift;
+  $self->_agent_plandata('agent_invoice_from');
+}
+
+sub _agent_plandata {
+  my( $self, $option ) = @_;
+
+  my $part_bill_event = qsearchs( 'part_bill_event',
     {
       'payby'     => $self->cust_main->payby,
       'plan'      => 'send_agent',
     {
       'payby'     => $self->cust_main->payby,
       'plan'      => 'send_agent',
-      'eventcode' => { 'op'    => 'LIKE',
-                       'value' => '_%, '. $self->cust_main->agentnum. ');' },
+      'plandata'  => { 'op'    => '~',
+                       'value' => "(^|\n)agentnum ".
+                                  $self->cust_main->agentnum.
+                                  "(\n|\$)",
+                     },
     },
     '',
     'ORDER BY seconds LIMIT 1'
   );
 
     },
     '',
     'ORDER BY seconds LIMIT 1'
   );
 
-  return '' unless $cust_bill_event;
+  return '' unless $part_bill_event;
 
 
-  if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) {
+  if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) {
     return $1;
   } else {
     return $1;
   } else {
-    warn "can't parse eventcode for agent-specific invoice template";
+    warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart.
+         " plandata for $option";
     return '';
   }
 
     return '';
   }
 
@@ -699,14 +983,16 @@ L<Time::Local> and L<Date::Parse> for conversion functions.
 
 =cut
 
 
 =cut
 
+#still some false laziness w/print_text
 sub print_text {
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
 sub print_text {
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
+
 #  my $invnum = $self->invnum;
 #  my $invnum = $self->invnum;
-  my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
+  my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
 
   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
@@ -750,7 +1036,8 @@ sub print_text {
         push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
         push @buf, [ $description,
                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($self->_date);
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
@@ -760,7 +1047,8 @@ sub print_text {
           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
         ];
         push @buf,
-          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
+          map { [ "  ". $_->[0]. ": ". $_->[1], '' ] }
+              $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate);
       }
 
       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
       }
 
       push @buf, map { [ "  $_", '' ] } $cust_bill_pkg->details;
@@ -888,8 +1176,10 @@ sub print_text {
     if $cust_main->address2;
   $FS::cust_bill::_template::address[$l++] =
     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
     if $cust_main->address2;
   $FS::cust_bill::_template::address[$l++] =
     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
-  $FS::cust_bill::_template::address[$l++] = $cust_main->country
-    unless $cust_main->country eq 'US';
+
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  $FS::cust_bill::_template::address[$l++] = code2country($cust_main->country)
+    unless $cust_main->country eq $countrydefault;
 
        #  #overdue? (variable for the template)
        #  $FS::cust_bill::_template::overdue = ( 
 
        #  #overdue? (variable for the template)
        #  $FS::cust_bill::_template::overdue = ( 
@@ -942,32 +1232,58 @@ sub print_latex {
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
 
   my( $self, $today, $template ) = @_;
   $today ||= time;
+  warn "FS::cust_bill::print_latex called on $self with suffix $template\n"
+    if $DEBUG;
 
 
-#  my $invnum = $self->invnum;
   my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
   my $cust_main = $self->cust_main;
   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
-    unless $cust_main->payname && $cust_main->payby ne 'CHEK';
+    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
 
   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( $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 @collect = ();
-  #my($description,$amount);
-  @buf = ();
-
   #create the template
   $template ||= $self->_agent_template;
   my $templatefile = 'invoice_latex';
   my $suffix = length($template) ? "_$template" : '';
   $templatefile .= $suffix;
   #create the template
   $template ||= $self->_agent_template;
   my $templatefile = 'invoice_latex';
   my $suffix = length($template) ? "_$template" : '';
   $templatefile .= $suffix;
-  my @invoice_template = $conf->config($templatefile)
+  my @invoice_template = map "$_\n", $conf->config($templatefile)
     or die "cannot load config file $templatefile";
 
     or die "cannot load config file $templatefile";
 
+  my($format, $text_template);
+  if ( grep { /^%%Detail/ } @invoice_template ) {
+    #change this to a die when the old code is removed
+    warn "old-style invoice template $templatefile; ".
+         "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
+    $format = 'old';
+  } else {
+    $format = 'Text::Template';
+    $text_template = new Text::Template(
+      TYPE => 'ARRAY',
+      SOURCE => \@invoice_template,
+      DELIMITERS => [ '[@--', '--@]' ],
+    );
+
+    $text_template->compile()
+      or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
+  }
+
+  my $returnaddress;
+  if ( $conf->exists('invoice_latexreturnaddress')
+       && length($conf->exists('invoice_latexreturnaddress'))
+     )
+  {
+    $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') );
+  } else {
+    $returnaddress = '~';
+  }
+
   my %invoice_data = (
     'invnum'       => $self->invnum,
     'date'         => time2str('%b %o, %Y', $self->_date),
   my %invoice_data = (
     'invnum'       => $self->invnum,
     'date'         => time2str('%b %o, %Y', $self->_date),
+    'today'        => time2str('%b %o, %Y', $today),
     'agent'        => _latex_escape($cust_main->agent->agent),
     'payname'      => _latex_escape($cust_main->payname),
     'company'      => _latex_escape($cust_main->company),
     'agent'        => _latex_escape($cust_main->agent->agent),
     'payname'      => _latex_escape($cust_main->payname),
     'company'      => _latex_escape($cust_main->company),
@@ -976,23 +1292,30 @@ sub print_latex {
     'city'         => _latex_escape($cust_main->city),
     'state'        => _latex_escape($cust_main->state),
     'zip'          => _latex_escape($cust_main->zip),
     'city'         => _latex_escape($cust_main->city),
     'state'        => _latex_escape($cust_main->state),
     'zip'          => _latex_escape($cust_main->zip),
-    'country'      => _latex_escape($cust_main->country),
     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
+    'returnaddress' => $returnaddress,
     'quantity'     => 1,
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
     'quantity'     => 1,
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
+    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
   );
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
   );
 
   my $countrydefault = $conf->config('countrydefault') || 'US';
-  $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
+  if ( $cust_main->country eq $countrydefault ) {
+    $invoice_data{'country'} = '';
+  } else {
+    $invoice_data{'country'} = _latex_escape(code2country($cust_main->country));
+  }
 
 
-  #do variable substitutions in notes
   $invoice_data{'notes'} =
     join("\n",
   $invoice_data{'notes'} =
     join("\n",
-      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-        $conf->config_orbase('invoice_latexnotes', $suffix)
+#  #do variable substitutions in notes
+#      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+        $conf->config_orbase('invoice_latexnotes', $template)
     );
     );
+  warn "invoice notes: ". $invoice_data{'notes'}. "\n"
+    if $DEBUG;
 
   $invoice_data{'footer'} =~ s/\n+$//;
   $invoice_data{'smallfooter'} =~ s/\n+$//;
 
   $invoice_data{'footer'} =~ s/\n+$//;
   $invoice_data{'smallfooter'} =~ s/\n+$//;
@@ -1003,110 +1326,200 @@ sub print_latex {
       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
       : '~';
 
       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
       : '~';
 
-  my @line_item = ();
-  my @total_item = ();
   my @filled_in = ();
   my @filled_in = ();
-  while ( @invoice_template ) {
-    my $line = shift @invoice_template;
-
-    if ( $line =~ /^%%Detail\s*$/ ) {
-
-      while ( ( my $line_item_line = shift @invoice_template )
-              !~ /^%%EndDetail\s*$/                            ) {
-        push @line_item, $line_item_line;
-      }
-      foreach my $line_item ( $self->_items ) {
-      #foreach my $line_item ( $self->_items_pkg ) {
-        $invoice_data{'ref'} = $line_item->{'pkgnum'};
-        $invoice_data{'description'} = _latex_escape($line_item->{'description'});
-        if ( exists $line_item->{'ext_description'} ) {
-          $invoice_data{'description'} .=
-            "\\tabularnewline\n~~".
-            join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
+  if ( $format eq 'old' ) {
+  
+    my @line_item = ();
+    my @total_item = ();
+    while ( @invoice_template ) {
+      my $line = shift @invoice_template;
+  
+      if ( $line =~ /^%%Detail\s*$/ ) {
+  
+        while ( ( my $line_item_line = shift @invoice_template )
+                !~ /^%%EndDetail\s*$/                            ) {
+          push @line_item, $line_item_line;
+        }
+        foreach my $line_item ( $self->_items ) {
+        #foreach my $line_item ( $self->_items_pkg ) {
+          $invoice_data{'ref'} = $line_item->{'pkgnum'};
+          $invoice_data{'description'} =
+            _latex_escape($line_item->{'description'});
+          if ( exists $line_item->{'ext_description'} ) {
+            $invoice_data{'description'} .=
+              "\\tabularnewline\n~~".
+              join( "\\tabularnewline\n~~",
+                    map _latex_escape($_), @{$line_item->{'ext_description'}}
+                  );
+          }
+          $invoice_data{'amount'} = $line_item->{'amount'};
+          $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+          push @filled_in,
+            map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
+        }
+  
+      } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
+  
+        while ( ( my $total_item_line = shift @invoice_template )
+                !~ /^%%EndTotalDetails\s*$/                      ) {
+          push @total_item, $total_item_line;
+        }
+  
+        my @total_fill = ();
+  
+        my $taxtotal = 0;
+        foreach my $tax ( $self->_items_tax ) {
+          $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
+          $taxtotal += $tax->{'amount'};
+          $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'};
+          push @total_fill,
+            map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+                @total_item;
         }
         }
-        $invoice_data{'amount'} = $line_item->{'amount'};
-        $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
-        push @filled_in,
-          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
-      }
-
-    } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
-
-      while ( ( my $total_item_line = shift @invoice_template )
-              !~ /^%%EndTotalDetails\s*$/                      ) {
-        push @total_item, $total_item_line;
-      }
-
-      my @total_fill = ();
 
 
-      my $taxtotal = 0;
-      foreach my $tax ( $self->_items_tax ) {
-        $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
-        $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
+        if ( $taxtotal ) {
+          $invoice_data{'total_item'} = 'Sub-total';
+          $invoice_data{'total_amount'} =
+            '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
+          unshift @total_fill,
+            map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+                @total_item;
+        }
+  
+        $invoice_data{'total_item'} = '\textbf{Total}';
+        $invoice_data{'total_amount'} =
+          '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
         push @total_fill,
           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
               @total_item;
         push @total_fill,
           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
               @total_item;
-      }
-
-      if ( $taxtotal ) {
-        $invoice_data{'total_item'} = 'Sub-total';
+  
+        #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
+  
+        # credits
+        foreach my $credit ( $self->_items_credits ) {
+          $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
+          #$credittotal
+          $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
+          push @total_fill, 
+            map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+                @total_item;
+        }
+  
+        # payments
+        foreach my $payment ( $self->_items_payments ) {
+          $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
+          #$paymenttotal
+          $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
+          push @total_fill, 
+            map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+                @total_item;
+        }
+  
+        $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
         $invoice_data{'total_amount'} =
         $invoice_data{'total_amount'} =
-          '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
-        unshift @total_fill,
+          '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
+        push @total_fill,
           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
               @total_item;
           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
               @total_item;
+  
+        push @filled_in, @total_fill;
+  
+      } else {
+        #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
+        $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
+        push @filled_in, $line;
       }
       }
+  
+    }
 
 
-      $invoice_data{'total_item'} = '\textbf{Total}';
-      $invoice_data{'total_amount'} =
-        '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
-      push @total_fill,
-        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-            @total_item;
-
-      #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
-
-      # credits
-      foreach my $credit ( $self->_items_credits ) {
-        $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
-        #$credittotal
-        $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
-        push @total_fill, 
-          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-              @total_item;
-      }
+    sub nounder {
+      my $var = $1;
+      $var =~ s/_/\-/g;
+      $var;
+    }
 
 
-      # payments
-      foreach my $payment ( $self->_items_payments ) {
-        $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
-        #$paymenttotal
-        $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
-        push @total_fill, 
-          map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-              @total_item;
+  } elsif ( $format eq 'Text::Template' ) {
+
+    my @detail_items = ();
+    my @total_items = ();
+
+    $invoice_data{'detail_items'} = \@detail_items;
+    $invoice_data{'total_items'} = \@total_items;
+  
+    foreach my $line_item ( $self->_items ) {
+      my $detail = {
+        ext_description => [],
+      };
+      $detail->{'ref'} = $line_item->{'pkgnum'};
+      $detail->{'quantity'} = 1;
+      $detail->{'description'} = _latex_escape($line_item->{'description'});
+      if ( exists $line_item->{'ext_description'} ) {
+        @{$detail->{'ext_description'}} = map {
+          _latex_escape($_);
+        } @{$line_item->{'ext_description'}};
       }
       }
-
-      $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
-      $invoice_data{'total_amount'} =
+      $detail->{'amount'} = $line_item->{'amount'};
+      $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+  
+      push @detail_items, $detail;
+    }
+  
+  
+    my $taxtotal = 0;
+    foreach my $tax ( $self->_items_tax ) {
+      my $total = {};
+      $total->{'total_item'} = _latex_escape($tax->{'description'});
+      $taxtotal += $tax->{'amount'};
+      $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
+      push @total_items, $total;
+    }
+  
+    if ( $taxtotal ) {
+      my $total = {};
+      $total->{'total_item'} = 'Sub-total';
+      $total->{'total_amount'} =
+        '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
+      unshift @total_items, $total;
+    }
+  
+    {
+      my $total = {};
+      $total->{'total_item'} = '\textbf{Total}';
+      $total->{'total_amount'} =
+        '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
+      push @total_items, $total;
+    }
+  
+    #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'} = _latex_escape($credit->{'description'});
+      #$credittotal
+      $total->{'total_amount'} = '-\dollar '. $credit->{'amount'};
+      push @total_items, $total;
+    }
+  
+    # payments
+    foreach my $payment ( $self->_items_payments ) {
+      my $total = {};
+      $total->{'total_item'} = _latex_escape($payment->{'description'});
+      #$paymenttotal
+      $total->{'total_amount'} = '-\dollar '. $payment->{'amount'};
+      push @total_items, $total;
+    }
+  
+    { 
+      my $total;
+      $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
+      $total->{'total_amount'} =
         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
-      push @total_fill,
-        map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
-            @total_item;
-
-      push @filled_in, @total_fill;
-
-    } else {
-      #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
-      $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
-      push @filled_in, $line;
+      push @total_items, $total;
     }
 
     }
 
-  }
-
-  sub nounder {
-    my $var = $1;
-    $var =~ s/_/\-/g;
-    $var;
+  } else {
+    die "guru meditation #54";
   }
 
   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
   }
 
   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
@@ -1115,7 +1528,13 @@ sub print_latex {
                            SUFFIX   => '.tex',
                            UNLINK   => 0,
                          ) or die "can't open temp file: $!\n";
                            SUFFIX   => '.tex',
                            UNLINK   => 0,
                          ) or die "can't open temp file: $!\n";
-  print $fh join("\n", @filled_in ), "\n";
+  if ( $format eq 'old' ) {
+    print $fh join('', @filled_in );
+  } elsif ( $format eq 'Text::Template' ) {
+    $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data);
+  } else {
+    die "guru meditation #32";
+  }
   close $fh;
 
   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
   close $fh;
 
   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
@@ -1142,13 +1561,15 @@ sub print_ps {
   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
   chdir($dir);
 
   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
   chdir($dir);
 
-  system("pslatex $file.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed: $!";
-  system("pslatex $file.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed: $!";
+  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('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
 
   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0
-    or die "dbips failed: $!";
+    or die "dvips failed";
 
   open(POSTSCRIPT, "<$file.ps")
     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
 
   open(POSTSCRIPT, "<$file.ps")
     or die "can't open $file.ps: $! (error in LaTeX template?)\n";
@@ -1189,15 +1610,17 @@ sub print_pdf {
   #system('pdflatex', "$file.tex");
   #! LaTeX Error: Unknown graphics extension: .eps.
 
   #system('pdflatex', "$file.tex");
   #! LaTeX Error: Unknown graphics extension: .eps.
 
-  system("pslatex $file.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed: $!";
-  system("pslatex $file.tex >/dev/null 2>&1") == 0
-    or die "pslatex $file.tex failed: $!";
+  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(
 
   #system('dvipdf', "$file.dvi", "$file.pdf" );
   system(
-    "dvips -q -t letter -f $file.dvi ".
-    "| gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf ".
+    "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: $!";
     "     -c save pop -"
   ) == 0
     or die "dvips | gs failed: $!";
@@ -1218,6 +1641,191 @@ sub print_pdf {
 
 }
 
 
 }
 
+=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
+
+Returns an HTML invoice, as a scalar.
+
+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
+when emailing the invoice as part of a multipart/related MIME email.
+
+=cut
+
+sub print_html {
+  my( $self, $today, $template, $cid ) = @_;
+  $today ||= time;
+
+  my $cust_main = $self->cust_main;
+  $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
+    unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
+
+  $template ||= $self->_agent_template;
+  my $templatefile = 'invoice_html';
+  my $suffix = length($template) ? "_$template" : '';
+  $templatefile .= $suffix;
+  my @html_template = map "$_\n", $conf->config($templatefile)
+    or die "cannot load config file $templatefile";
+
+  my $html_template = new Text::Template(
+    TYPE   => 'ARRAY',
+    SOURCE => \@html_template,
+    DELIMITERS => [ '<%=', '%>' ],
+  );
+
+  $html_template->compile()
+    or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
+
+  my %invoice_data = (
+    'invnum'       => $self->invnum,
+    'date'         => time2str('%b&nbsp;%o,&nbsp;%Y', $self->_date),
+    'today'        => time2str('%b %o, %Y', $today),
+    'agent'        => encode_entities($cust_main->agent->agent),
+    'payname'      => encode_entities($cust_main->payname),
+    'company'      => encode_entities($cust_main->company),
+    'address1'     => encode_entities($cust_main->address1),
+    'address2'     => encode_entities($cust_main->address2),
+    'city'         => encode_entities($cust_main->city),
+    'state'        => encode_entities($cust_main->state),
+    'zip'          => encode_entities($cust_main->zip),
+    'terms'        => $conf->config('invoice_default_terms')
+                      || 'Payable upon receipt',
+    'cid'          => $cid,
+#    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+  );
+
+  $invoice_data{'returnaddress'} = $conf->exists('invoice_htmlreturnaddress')
+    ? join("\n", $conf->config('invoice_htmlreturnaddress') )
+    : join("\n", map { 
+                       s/~/&nbsp;/g;
+                       s/\\\\\*?\s*$/<BR>/;
+                       s/\\hyphenation\{[\w\s\-]+\}//;
+                       $_;
+                     }
+                     $conf->config('invoice_latexreturnaddress')
+          );
+
+  my $countrydefault = $conf->config('countrydefault') || 'US';
+  if ( $cust_main->country eq $countrydefault ) {
+    $invoice_data{'country'} = '';
+  } else {
+    $invoice_data{'country'} =
+      encode_entities(code2country($cust_main->country));
+  }
+
+  $invoice_data{'notes'} =
+    length($conf->config_orbase('invoice_htmlnotes', $template))
+      ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) )
+      : join("\n", map { 
+                         s/%%(.*)$/<!-- $1 -->/;
+                         s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/;
+                         s/\\begin\{enumerate\}/<ol>/;
+                         s/\\item /  <li>/;
+                         s/\\end\{enumerate\}/<\/ol>/;
+                         s/\\textbf\{(.*)\}/<b>$1<\/b>/;
+                         $_;
+                       } 
+                       $conf->config_orbase('invoice_latexnotes', $template)
+            );
+
+#  #do variable substitutions in notes
+#  $invoice_data{'notes'} =
+#    join("\n",
+#      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
+#        $conf->config_orbase('invoice_latexnotes', $suffix)
+#    );
+
+   $invoice_data{'footer'} = $conf->exists('invoice_htmlfooter')
+     ? join("\n", $conf->config('invoice_htmlfooter') )
+     : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
+                      $conf->config('invoice_latexfooter')
+           );
+
+  $invoice_data{'po_line'} =
+    (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
+      ? encode_entities("Purchase Order #". $cust_main->payinfo)
+      : '';
+
+  my $money_char = $conf->config('money_char') || '$';
+
+  foreach my $line_item ( $self->_items ) {
+    my $detail = {
+      ext_description => [],
+    };
+    $detail->{'ref'} = $line_item->{'pkgnum'};
+    $detail->{'description'} = encode_entities($line_item->{'description'});
+    if ( exists $line_item->{'ext_description'} ) {
+      @{$detail->{'ext_description'}} = map {
+        encode_entities($_);
+      } @{$line_item->{'ext_description'}};
+    }
+    $detail->{'amount'} = $money_char. $line_item->{'amount'};
+    $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
+
+    push @{$invoice_data{'detail_items'}}, $detail;
+  }
+
+
+  my $taxtotal = 0;
+  foreach my $tax ( $self->_items_tax ) {
+    my $total = {};
+    $total->{'total_item'} = encode_entities($tax->{'description'});
+    $taxtotal += $tax->{'amount'};
+    $total->{'total_amount'} = $money_char. $tax->{'amount'};
+    push @{$invoice_data{'total_items'}}, $total;
+  }
+
+  if ( $taxtotal ) {
+    my $total = {};
+    $total->{'total_item'} = 'Sub-total';
+    $total->{'total_amount'} =
+      $money_char. sprintf('%.2f', $self->charged - $taxtotal );
+    unshift @{$invoice_data{'total_items'}}, $total;
+  }
+
+  my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
+  {
+    my $total = {};
+    $total->{'total_item'} = '<b>Total</b>';
+    $total->{'total_amount'} =
+      "<b>$money_char".  sprintf('%.2f', $self->charged + $pr_total ). '</b>';
+    push @{$invoice_data{'total_items'}}, $total;
+  }
+
+  #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'} = encode_entities($credit->{'description'});
+    #$credittotal
+    $total->{'total_amount'} = "-$money_char". $credit->{'amount'};
+    push @{$invoice_data{'total_items'}}, $total;
+  }
+
+  # payments
+  foreach my $payment ( $self->_items_payments ) {
+    my $total = {};
+    $total->{'total_item'} = encode_entities($payment->{'description'});
+    #$paymenttotal
+    $total->{'total_amount'} = "-$money_char". $payment->{'amount'};
+    push @{$invoice_data{'total_items'}}, $total;
+  }
+
+  { 
+    my $total;
+    $total->{'total_item'} = '<b>'. $self->balance_due_msg. '</b>';
+    $total->{'total_amount'} =
+      "<b>$money_char".  sprintf('%.2f', $self->owed + $pr_total ). '</b>';
+    push @{$invoice_data{'total_items'}}, $total;
+  }
+
+  $html_template->fill_in( HASH => \%invoice_data);
+}
+
 # quick subroutine for print_latex
 #
 # There are ten characters that LaTeX treats as special characters, which
 # quick subroutine for print_latex
 #
 # There are ten characters that LaTeX treats as special characters, which
@@ -1229,7 +1837,8 @@ sub print_pdf {
 
 sub _latex_escape {
   my $value = shift;
 
 sub _latex_escape {
   my $value = shift;
-  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
+  $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
+  $value =~ s/([<>])/\$$1\$/g;
   $value;
 }
 
   $value;
 }
 
@@ -1272,7 +1881,7 @@ sub _items_previous {
                        ' ('. time2str('%x',$_->_date). ')',
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
                        ' ('. time2str('%x',$_->_date). ')',
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%10.2f", $_->owed),
+      'amount'      => sprintf("%.2f", $_->owed),
     };
   }
   @b;
     };
   }
   @b;
@@ -1316,45 +1925,32 @@ sub _items_cust_bill_pkg {
       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
       my $pkg = $part_pkg->pkg;
 
       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
       my $pkg = $part_pkg->pkg;
 
-      my %labels;
-      #tie %labels, 'Tie::IxHash';
-      push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
-      my @ext_description;
-      foreach my $label ( keys %labels ) {
-        my @values = @{ $labels{$label} };
-        my $num = scalar(@values);
-        if ( $num > 5 ) {
-          push @ext_description, "$label ($num)";
-        } else {
-          push @ext_description, map { "$label: $_" } @values;
-        }
-      }
-
       if ( $cust_bill_pkg->setup != 0 ) {
         my $description = $pkg;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
       if ( $cust_bill_pkg->setup != 0 ) {
         my $description = $pkg;
         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
-        my @d = @ext_description;
+        my @d = $cust_pkg->h_labels_short($self->_date);
         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
         push @b, {
         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
         push @b, {
-          'description'     => $description,
-          #'pkgpart'         => $part_pkg->pkgpart,
-          'pkgnum'          => $cust_pkg->pkgnum,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
-          'ext_description' => \@d,
+          description     => $description,
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%.2f", $cust_bill_pkg->setup),
+          ext_description => \@d,
         };
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
         };
       }
 
       if ( $cust_bill_pkg->recur != 0 ) {
         push @b, {
-          'description'     => "$pkg (" .
+          description     => "$pkg (" .
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
                                time2str('%x', $cust_bill_pkg->edate). ')',
-          #'pkgpart'         => $part_pkg->pkgpart,
-          'pkgnum'          => $cust_pkg->pkgnum,
-          'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
-          'ext_description' => [ @ext_description,
-                                 $cust_bill_pkg->details,
-                               ],
+          #pkgpart         => $part_pkg->pkgpart,
+          pkgnum          => $cust_pkg->pkgnum,
+          amount          => sprintf("%.2f", $cust_bill_pkg->recur),
+          ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate,
+                                                         $cust_bill_pkg->sdate),
+                               $cust_bill_pkg->details,
+                             ],
         };
       }
 
         };
       }
 
@@ -1366,7 +1962,7 @@ sub _items_cust_bill_pkg {
       if ( $cust_bill_pkg->setup != 0 ) {
         push @b, {
           'description' => $itemdesc,
       if ( $cust_bill_pkg->setup != 0 ) {
         push @b, {
           'description' => $itemdesc,
-          'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
+          'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
         };
       }
       if ( $cust_bill_pkg->recur != 0 ) {
         };
       }
       if ( $cust_bill_pkg->recur != 0 ) {
@@ -1374,7 +1970,7 @@ sub _items_cust_bill_pkg {
           'description' => "$itemdesc (".
                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
                            time2str("%x", $cust_bill_pkg->edate). ')',
           'description' => "$itemdesc (".
                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
                            time2str("%x", $cust_bill_pkg->edate). ')',
-          'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
+          'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
         };
       }
 
         };
       }
 
@@ -1403,9 +1999,9 @@ sub _items_credits {
       #'description' => 'Credit ref\#'. $_->crednum.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
       #'description' => 'Credit ref\#'. $_->crednum.
       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
       #                 $reason,
-      'description' => 'Credit applied'.
+      'description' => 'Credit applied '.
                        time2str("%x",$_->cust_credit->_date). $reason,
                        time2str("%x",$_->cust_credit->_date). $reason,
-      'amount'      => sprintf("%10.2f",$_->amount),
+      'amount'      => sprintf("%.2f",$_->amount),
     };
   }
   #foreach ( @cr_cust_credit ) {
     };
   }
   #foreach ( @cr_cust_credit ) {
@@ -1431,7 +2027,7 @@ sub _items_payments {
     push @b, {
       'description' => "Payment received ".
                        time2str("%x",$_->cust_pay->_date ),
     push @b, {
       'description' => "Payment received ".
                        time2str("%x",$_->cust_pay->_date ),
-      'amount'      => sprintf("%10.2f", $_->amount )
+      'amount'      => sprintf("%.2f", $_->amount )
     };
   }
 
     };
   }