agent-specific logos for html invoices too
[freeside.git] / FS / FS / cust_bill.pm
index a01880c..7d10411 100644 (file)
@@ -1,12 +1,14 @@
 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 Text::Template 1.20;
 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::Misc qw( send_email send_fax );
@@ -18,9 +20,14 @@ use FS::cust_pkg;
 use FS::cust_credit_bill;
 use FS::cust_pay_batch;
 use FS::cust_bill_event;
+use FS::part_pkg;
+use FS::cust_bill_pay;
+use FS::part_bill_event;
 
 @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;
@@ -339,57 +346,208 @@ 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 $mimeparts;
-  if ($conf->exists('invoice_email_pdf')) {
-    #warn "[FS::cust_bill::send] creating PDF attachment";
-    #mime parts arguments a la MIME::Entity->build().
-    $mimeparts = [
-      {
-        'Type'        => 'application/pdf',
-        'Encoding'    => 'base64',
-        'Data'        => [ $self->print_pdf('', $args{'template'}) ],
-        'Disposition' => 'attachment',
-        'Filename'    => 'invoice.pdf',
-      },
-    ];
-  }
+  my $me = '[FS::cust_bill::generate_email]';
 
-  my $email_text;
-  if ($conf->exists('invoice_email_pdf')
-      and scalar($conf->config('invoice_email_pdf_note'))) {
+  my %return = (
+    'from'      => $args{'from'},
+    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
+  );
 
-    #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'";
-    $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ];
+  if (ref($args{'to'} eq 'ARRAY')) {
+    $return{'to'} = $args{'to'};
   } else {
-    #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'";
-    if (ref($args{'print_text'}) eq 'ARRAY') {
-      $email_text = $args{'print_text'};
+    $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 {
-      $email_text = [ $self->print_text('', $args{'template'}) ];
+
+      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'}) ];
+      }
+
     }
-  }
 
-  my @invoicing_list;
-  if (ref($args{'to'} eq 'ARRAY')) {
-    @invoicing_list = @{$args{'to'}};
+    $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 $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
+    my $file;
+    if ( [ -e "$path/logo_". $args{'_template'}. ".png" ] ) {
+      $file = "$path/logo_". $args{'_template'}. ".png";
+    } else {
+      $file = "$path/logo.png";
+    }
+
+    my $image = build MIME::Entity
+      'Type'       => 'image/png',
+      'Encoding'   => 'base64',
+      'Path'       => $file,
+      '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 {
-    @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list;
+
+    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 (
-    'from'      => $args{'from'},
-    'to'        => [ @invoicing_list ],
-    'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
-    'body'      => $email_text,
-    'mimeparts' => $mimeparts,
-  );
+  %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 ] ] ]
@@ -400,7 +558,8 @@ emails or print.  See L<FS::cust_main_invoice>.
 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.
+of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
+single agent) or an arrayref of agentnums.
 
 INVOICE_FROM, if specified, overrides the default email invoice From: address.
 
@@ -409,63 +568,125 @@ INVOICE_FROM, if specified, overrides the default email invoice From: address.
 sub send {
   my $self = shift;
   my $template = scalar(@_) ? shift : '';
-  return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift;
+  if ( scalar(@_) && $_[0]  ) {
+    my $agentnums = ref($_[0]) ? shift : [ shift ];
+    return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
+  }
+
   my $invoice_from =
     scalar(@_)
       ? shift
       : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
 
-  my @print_text = $self->print_text('', $template);
   my @invoicing_list = $self->cust_main->invoicing_list;
 
-  if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list  ) {
-    #email
+  $self->email($template, $invoice_from)
+    if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
 
-    #better to notify this person than silence
-    @invoicing_list = ($invoice_from) unless @invoicing_list;
+  $self->print($template)
+    if grep { $_ eq 'POST' } @invoicing_list; #postal
 
-    my $error = send_email(
-      $self->generate_email(
-        'from'       => $invoice_from,
-        'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
-        'print_text' => [ @print_text ],
-      )
-    );
-    die "can't email invoice: $error\n" if $error;
-    #die "$error\n" if $error;
+  $self->fax($template)
+    if grep { $_ eq 'FAX' } @invoicing_list; #fax
 
-  }
+  '';
 
-  if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) {
-    my $lpr_data;
-    if ($conf->config('invoice_latex')) {
-      $lpr_data = [ $self->print_ps('', $template) ];
-    } else {
-      $lpr_data = \@print_text;
-    }
+}
 
-    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";
-    }
+=item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
 
-    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;
-    }
+Emails this invoice.
 
-  }
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
 
-  '';
+INVOICE_FROM, if specified, overrides the default email invoice From: address.
+
+=cut
+
+sub email {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+  my $invoice_from =
+    scalar(@_)
+      ? shift
+      : ( $self->_agent_invoice_from || $conf->config('invoice_from') );
+
+  my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
+                            $self->cust_main->invoicing_list;
+
+  #better to notify this person than silence
+  @invoicing_list = ($invoice_from) unless @invoicing_list;
+
+  my $error = send_email(
+    $self->generate_email(
+      'from'       => $invoice_from,
+      'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
+      'template'   => $template,
+    )
+  );
+  die "can't email invoice: $error\n" if $error;
+  #die "$error\n" if $error;
+
+}
+
+=item lpr_data [ TEMPLATENAME ]
+
+Returns the postscript or plaintext for this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub lpr_data {
+  my( $self, $template) = @_;
+  $conf->exists('invoice_latex')
+    ? [ $self->print_ps('', $template) ]
+    : [ $self->print_text('', $template) ];
+}
+
+=item print [ TEMPLATENAME ]
+
+Prints this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub print {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  my $lpr = $conf->config('lpr');
+  open(LPR, "|$lpr")
+    or die "Can't open pipe to $lpr: $!\n";
+  print LPR @{ $self->lpr_data($template) };
+  close LPR
+    or die $! ? "Error closing $lpr: $!\n"
+              : "Exit status $? from $lpr\n";
+}
+
+=item fax [ TEMPLATENAME ] 
+
+Faxes this invoice.
+
+TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
+
+=cut
+
+sub fax {
+  my $self = shift;
+  my $template = scalar(@_) ? shift : '';
+
+  die 'FAX invoice destination not (yet?) supported with plain text invoices.'
+    unless $conf->exists('invoice_latex');
+
+  my $dialstring = $self->cust_main->getfield('fax');
+  #Check $dialstring?
+
+  my $error = send_fax( 'docdata'    => $self->lpr_data($template),
+                        'dialstring' => $dialstring,
+                      );
+  die $error if $error;
 
 }
 
@@ -804,7 +1025,9 @@ sub _agent_plandata {
       'plan'      => 'send_agent',
       'plandata'  => { 'op'    => '~',
                        'value' => "(^|\n)agentnum ".
+                                   '([0-9]*, )*'.
                                   $self->cust_main->agentnum.
+                                   '(, [0-9]*)*'.
                                   "(\n|\$)",
                      },
     },
@@ -876,7 +1099,7 @@ sub print_text {
     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
   ) {
 
-    if ( $cust_bill_pkg->pkgnum ) {
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
 
       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
@@ -1028,8 +1251,10 @@ sub print_text {
     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 = ( 
@@ -1082,8 +1307,9 @@ sub print_latex {
 
   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') )
     unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/;
@@ -1093,10 +1319,6 @@ sub print_latex {
   #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';
@@ -1123,9 +1345,19 @@ sub print_latex {
       or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
   }
 
+  my $returnaddress;
+  if ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) {
+    $returnaddress = join("\n",
+      $conf->config_orbase('invoice_latexreturnaddress', $template)
+    );
+  } else {
+    $returnaddress = '~';
+  }
+
   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),
@@ -1134,10 +1366,9 @@ sub print_latex {
     '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') ),
-    'returnaddress' => join("\n", $conf->config('invoice_latexreturnaddress') ),
+    'footer'       => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ),
+    'smallfooter'  => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ),
+    'returnaddress' => $returnaddress,
     'quantity'     => 1,
     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
@@ -1145,14 +1376,20 @@ sub print_latex {
   );
 
   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",
-      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+$//;
@@ -1207,12 +1444,13 @@ sub print_latex {
         my $taxtotal = 0;
         foreach my $tax ( $self->_items_tax ) {
           $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
-          $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
+          $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;
         }
-  
+
         if ( $taxtotal ) {
           $invoice_data{'total_item'} = 'Sub-total';
           $invoice_data{'total_amount'} =
@@ -1305,7 +1543,8 @@ sub print_latex {
     foreach my $tax ( $self->_items_tax ) {
       my $total = {};
       $total->{'total_item'} = _latex_escape($tax->{'description'});
-      $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
+      $taxtotal += $tax->{'amount'};
+      $total->{'total_amount'} = '\dollar '. $tax->{'amount'};
       push @total_items, $total;
     }
   
@@ -1476,6 +1715,194 @@ 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,
+    'template'     => $template,
+#    'conf_dir'     => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
+  );
+
+  $invoice_data{'returnaddress'} =
+    length( $conf->config_orbase('invoice_htmlreturnaddress', $template) )
+      ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) )
+      : join("\n", map { 
+                         s/~/&nbsp;/g;
+                         s/\\\\\*?\s*$/<BR>/;
+                         s/\\hyphenation\{[\w\s\-]+\}//;
+                         $_;
+                       }
+                       $conf->config_orbase('invoice_latexreturnaddress', $template)
+            );
+
+  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'} =
+     length($conf->config_orbase('invoice_htmlfooter', $template))
+       ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) )
+       : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; }
+                        $conf->config_orbase('invoice_latexfooter', $template)
+             );
+
+  $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
@@ -1531,7 +1958,7 @@ sub _items_previous {
                        ' ('. time2str('%x',$_->_date). ')',
       #'pkgpart'     => 'N/A',
       'pkgnum'      => 'N/A',
-      'amount'      => sprintf("%10.2f", $_->owed),
+      'amount'      => sprintf("%.2f", $_->owed),
     };
   }
   @b;
@@ -1569,7 +1996,7 @@ sub _items_cust_bill_pkg {
   my @b = ();
   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
 
-    if ( $cust_bill_pkg->pkgnum ) {
+    if ( $cust_bill_pkg->pkgnum > 0 ) {
 
       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
@@ -1584,7 +2011,7 @@ sub _items_cust_bill_pkg {
           description     => $description,
           #pkgpart         => $part_pkg->pkgpart,
           pkgnum          => $cust_pkg->pkgnum,
-          amount          => sprintf("%10.2f", $cust_bill_pkg->setup),
+          amount          => sprintf("%.2f", $cust_bill_pkg->setup),
           ext_description => \@d,
         };
       }
@@ -1596,7 +2023,7 @@ sub _items_cust_bill_pkg {
                                time2str('%x', $cust_bill_pkg->edate). ')',
           #pkgpart         => $part_pkg->pkgpart,
           pkgnum          => $cust_pkg->pkgnum,
-          amount          => sprintf("%10.2f", $cust_bill_pkg->recur),
+          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,
@@ -1612,7 +2039,7 @@ sub _items_cust_bill_pkg {
       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 ) {
@@ -1620,7 +2047,7 @@ sub _items_cust_bill_pkg {
           '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),
         };
       }
 
@@ -1651,7 +2078,7 @@ sub _items_credits {
       #                 $reason,
       'description' => 'Credit applied '.
                        time2str("%x",$_->cust_credit->_date). $reason,
-      'amount'      => sprintf("%10.2f",$_->amount),
+      'amount'      => sprintf("%.2f",$_->amount),
     };
   }
   #foreach ( @cr_cust_credit ) {
@@ -1677,7 +2104,7 @@ sub _items_payments {
     push @b, {
       'description' => "Payment received ".
                        time2str("%x",$_->cust_pay->_date ),
-      'amount'      => sprintf("%10.2f", $_->amount )
+      'amount'      => sprintf("%.2f", $_->amount )
     };
   }