html invoices!
authorivan <ivan>
Sat, 14 May 2005 16:27:26 +0000 (16:27 +0000)
committerivan <ivan>
Sat, 14 May 2005 16:27:26 +0000 (16:27 +0000)
http://chris-linfoot.net/d6plinks/CWLT-5VZD4Y
http://www.dsv.su.se/~jpalme/ietf/mhtml.html
ftp://ftp.dsv.su.se/users/jpalme/draft-ietf-mhtml-info.txt
http://mailformat.dan.info/headers/mime.html
http://www.faqs.org/rfcs/rfc2392.html
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cdosys/html/_cdosys_content-type_multipart.asp

(MIME is hard, let's go shopping!)

ANNOUNCE.1.5
FS/FS/Conf.pm
FS/FS/Misc.pm
FS/FS/cust_bill.pm
FS/FS/part_bill_event.pm
conf/invoice_html [new file with mode: 0644]
conf/logo.png [new file with mode: 0644]
httemplate/docs/billing.html
httemplate/view/cust_bill.cgi

index 7ffd8bd..ce3d99d 100644 (file)
@@ -43,6 +43,8 @@
 - reformatted latex invoice templates w/Text::Template (khoff) and removed
   some useless fields (quantity/unit price)
 - simplified upgrade instructions
+- add export to vpopmail SQL
+- html invoices
 
 notyet (1.5.8?):
 - account merging UI in exports (for example, to consolidate passwd files from
index be28297..15ac23d 100644 (file)
@@ -209,6 +209,31 @@ sub config_items {
         new FS::ConfItem {
                            'key'         => $basename,
                            'section'     => 'billing',
+                           'description' => 'Alternate HTML template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+                           'type'        => 'textarea',
+                         }
+      } glob($self->dir. '/invoice_html_*')
+  ),
+  ( map { 
+        my $basename = basename($_);
+        $basename =~ /^(.*)$/;
+        $basename = $1;
+        ($latexname = $basename ) =~ s/latex/html/;
+        new FS::ConfItem {
+                           'key'         => $basename,
+                           'section'     => 'billing',
+                           'description' => "Alternate Notes section for HTML invoices.  Defaults to the same data in $latexname if not specified.",
+                           'type'        => 'textarea',
+                         }
+      } glob($self->dir. '/invoice_htmlnotes_*')
+  ),
+  ( map { 
+        my $basename = basename($_);
+        $basename =~ /^(.*)$/;
+        $basename = $1;
+        new FS::ConfItem {
+                           'key'         => $basename,
+                           'section'     => 'billing',
                            'description' => 'Alternate LaTeX template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
                            'type'        => 'textarea',
                          }
@@ -540,9 +565,38 @@ httemplate/docs/config.html
   },
 
   {
+    'key'         => 'invoice_html',
+    'section'     => 'billing',
+    'description' => 'Optional HTML template for invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
+
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_htmlnotes',
+    'section'     => 'billing',
+    'description' => 'Notes section for HTML invoices.  Defaults to the same data in invoice_latexnotes if not specified.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_htmlfooter',
+    'section'     => 'billing',
+    'description' => 'Footer for HTML invoices.  Defaults to the same data in invoice_latexfooter if not specified.',
+    'type'        => 'textarea',
+  },
+
+  {
+    'key'         => 'invoice_htmlreturnaddress',
+    'section'     => 'billing',
+    'description' => 'Return address for HTML invoices.  Defaults to the same data in invoice_latexreturnaddress if not specified.',
+    'type'        => 'textarea',
+  },
+
+  {
     'key'         => 'invoice_latex',
     'section'     => 'billing',
-    'description' => 'Optional LaTeX template for typeset PostScript invoices.',
+    'description' => 'Optional LaTeX template for typeset PostScript invoices.  See the <a href="../docs/billing.html">billing documentation</a> for details.',
     'type'        => 'textarea',
   },
 
index 7d7b7d0..92780f7 100644 (file)
@@ -1,12 +1,15 @@
 package FS::Misc;
 
 use strict;
-use vars qw ( @ISA @EXPORT_OK );
+use vars qw ( @ISA @EXPORT_OK $DEBUG );
 use Exporter;
+use Carp;
 
 @ISA = qw( Exporter );
 @EXPORT_OK = qw( send_email send_fax );
 
+$DEBUG = 1;
+
 =head1 NAME
 
 FS::Misc - Miscellaneous subroutines
@@ -37,11 +40,19 @@ I<to> - (required) comma-separated scalar or arrayref of recipients
 
 I<subject> - (required)
 
-I<content-type> - (optional) MIME type
+I<content-type> - (optional) MIME type for the body
+
+I<body> - (required unless I<nobody> is true) arrayref of body text lines
+
+I<mimeparts> - (optional, but required if I<nobody> is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects.  These will be passed as arguments to MIME::Entity->attach().
+
+I<nobody> - (optional) when set true, send_email will ignore the I<body> option and simply construct a message with the given I<mimeparts>.  In this case,
+I<content-type>, if specified, overrides the default "multipart/mixed" for the outermost MIME container.
 
-I<body> - (required) arrayref of body text lines
+I<content-encoding> - (optional) when using nobody, optional top-level MIME
+encoding which, if specified, overrides the default "7bit".
 
-I<mimeparts> - (optional) arrayref of MIME::Entity->build PARAMHASH refs, not MIME::Entity objects.  These will be passed as arguments to MIME::Entity->attach().
+I<type> - (optional) type parameter for multipart/related messages
 
 =cut
 
@@ -62,44 +73,93 @@ sub send_email {
   $ENV{MAILADDRESS} = $options{'from'};
   my $to = ref($options{to}) ? join(', ', @{ $options{to} } ) : $options{to};
 
-  my @mimeparts = (ref($options{'mimeparts'}) eq 'ARRAY')
-                  ? @{$options{'mimeparts'}} : ();
-  my $mimetype = (scalar(@mimeparts)) ? 'multipart/mixed' : 'text/plain';
+  my @mimeargs = ();
+  my @mimeparts = ();
+  if ( $options{'nobody'} ) {
+
+    croak "'mimeparts' option required when 'nobody' option given\n"
+      unless $options{'mimeparts'};
+
+    @mimeparts = @{$options{'mimeparts'}};
 
-  my @mimeargs;
-  if (scalar(@mimeparts)) {
     @mimeargs = (
-      'Type'  => 'multipart/mixed',
+      'Type'         => ( $options{'content-type'} || 'multipart/mixed' ),
+      'Encoding'     => ( $options{'content-encoding'} || '7bit' ),
     );
 
-    push @mimeparts,
-      { 
+  } else {
+
+    @mimeparts = @{$options{'mimeparts'}}
+      if ref($options{'mimeparts'}) eq 'ARRAY';
+
+    if (scalar(@mimeparts)) {
+
+      @mimeargs = (
+        'Type'     => 'multipart/mixed',
+        'Encoding' => '7bit',
+      );
+  
+      unshift @mimeparts, { 
+        'Type'        => ( $options{'content-type'} || 'text/plain' ),
         'Data'        => $options{'body'},
+        'Encoding'    => ( $options{'content-type'} ? '-SUGGEST' : '7bit' ),
         'Disposition' => 'inline',
-        'Type'        => (($options{'content-type'} ne '')
-                          ? $options{'content-type'} : 'text/plain'),
       };
-  } else {
-    @mimeargs = (
-      'Type'  => (($options{'content-type'} ne '')
-                  ? $options{'content-type'} : 'text/plain'),
-      'Data'  => $options{'body'},
-    );
+
+    } else {
+    
+      @mimeargs = (
+        'Type'     => ( $options{'content-type'} || 'text/plain' ),
+        'Data'     => $options{'body'},
+        'Encoding' => ( $options{'content-type'} ? '-SUGGEST' : '7bit' ),
+      );
+
+    }
+
   }
 
+  $options{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com';
+  my $message_id = join('.', rand()*(2**32), $$, time). "\@$1";
+
   my $message = MIME::Entity->build(
-    'From'      =>    $options{'from'},
-    'To'        =>    $to,
-    'Sender'    =>    $options{'from'},
-    'Reply-To'  =>    $options{'from'},
-    'Date'      =>    time2str("%a, %d %b %Y %X %z", time),
-    'Subject'   =>    $options{'subject'},
+    'From'       => $options{'from'},
+    'To'         => $to,
+    'Sender'     => $options{'from'},
+    'Reply-To'   => $options{'from'},
+    'Date'       => time2str("%a, %d %b %Y %X %z", time),
+    'Subject'    => $options{'subject'},
+    'Message-ID' => "<$message_id>",
     @mimeargs,
   );
 
+  if ( $options{'type'} ) {
+    #false laziness w/cust_bill::generate_email
+    $message->head->replace('Content-type',
+      $message->mime_type.
+      '; boundary="'. $message->head->multipart_boundary. '"'.
+      '; type='. $options{'type'}
+    );
+  }
+
   foreach my $part (@mimeparts) {
-    next unless ref($part) eq 'HASH'; #warn?
-    $message->attach(%$part);
+
+    if ( UNIVERSAL::isa($part, 'MIME::Entity') ) {
+
+      warn "attaching MIME part from MIME::Entity object\n"
+        if $DEBUG;
+      $message->add_part($part);
+
+    } elsif ( ref($part) eq 'HASH' ) {
+
+      warn "attaching MIME part from hashref:\n".
+           join("\n", map "  $_: ".$part->{$_}, keys %$part ). "\n"
+        if $DEBUG;
+      $message->attach(%$part);
+
+    } else {
+      croak "mimepart $part isn't a hashref or MIME::Entity object!";
+    }
+
   }
 
   my $smtpmachine = $conf->config('smtpmachine');
index fe5a653..f62f112 100644 (file)
@@ -343,57 +343,200 @@ 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 $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 {
-    @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 ] ] ]
@@ -419,7 +562,7 @@ sub send {
       ? 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;
 
   if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list  ) {
@@ -432,7 +575,8 @@ sub send {
       $self->generate_email(
         'from'       => $invoice_from,
         'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
-        'print_text' => [ @print_text ],
+        #'print_text' => [ @print_text ],
+        'template'   => $template,
       )
     );
     die "can't email invoice: $error\n" if $error;
@@ -445,7 +589,7 @@ sub send {
     if ($conf->config('invoice_latex')) {
       $lpr_data = [ $self->print_ps('', $template) ];
     } else {
-      $lpr_data = \@print_text;
+      $lpr_data = [ $self->print_text('', $template) ];
     }
 
     if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
@@ -1164,14 +1308,14 @@ sub print_latex {
     $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', $template)
-    );
-  warn "invoice notes: ". $invoice_data{'notes'}. "\n"
-    if $DEBUG;
+#  #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', $template)
+#    );
+#  warn "invoice notes: ". $invoice_data{'notes'}. "\n"
+#    if $DEBUG;
 
   $invoice_data{'footer'} =~ s/\n+$//;
   $invoice_data{'smallfooter'} =~ s/\n+$//;
@@ -1497,7 +1641,7 @@ sub print_pdf {
 
 }
 
-=item print_html [ TIME [ , TEMPLATE ] ]
+=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
 
 Returns an HTML invoice, as a scalar.
 
@@ -1506,76 +1650,13 @@ 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.
 
-=cut
+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.
 
-#sub print_html {
-#  my $self = shift;
-#
-#  my $file = $self->print_latex(@_);
-#
-#  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
-#  chdir($dir);
-#
-#  my $sfile = shell_quote $file;
-#
-#  system("htlatex $sfile.tex") == 0
-#    or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
-#  #system("ltoh $sfile.tex") == 0
-#  #  or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
-#
-#  open(HTML, "<$file.html")
-#    or die "can't open $file.html: $! (error in LaTeX template?)\n";
-#
-#  #unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
-#
-#  my $html = '';
-#  while (<HTML>) {
-#
-#    s/<link\s+rel="stylesheet"\s+type="text\/css"\s+href="invoice\.(\d+)\.(\w+)\.css">/<link rel="stylesheet" type="text\/css" href="cust_bill.html?$1.$2.css">/;
-##    s/<link\s+//;
-#    $html .= $_;
-#  }
-#
-#  close HTML;
-#
-#  return $html;
-#
-#}
-#
-##inefficient proof-of-concept for now
-#sub print_html_css {
-#  my $self = shift;
-#
-#  my $file = $self->print_latex(@_);
-#
-#  my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
-#  chdir($dir);
-#
-#  my $sfile = shell_quote $file;
-#
-#  system("htlatex $sfile.tex") == 0
-#    or die "hlatex $file.tex failed; is hlatex installed, or see $file.log for details?\n";
-#  #system("ltoh $sfile.tex") == 0
-#  #  or die "ltoh $file.tex failed; is hlatex installed, or see $file.log for details?\n";
-#
-#  open(CSS, "<$file.css")
-#    or die "can't open $file.html: $! (error in LaTeX template?)\n";
-#
-#  unlink("$file.dvi", "$file.log", "$file.aux", "$file.html", "$file.tex");
-#
-#  my $css = '';
-#  while (<CSS>) {
-#    $css .= $_;
-#  }
-#
-#  close CSS;
-#
-#  return $css;
-#
-#}
+=cut
 
 sub print_html {
-  my( $self, $today, $template ) = @_;
+  my( $self, $today, $template, $cid ) = @_;
   $today ||= time;
 
   my $cust_main = $self->cust_main;
@@ -1598,17 +1679,10 @@ sub print_html {
   $html_template->compile()
     or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR;
 
-  my $returnaddress = $conf->exists('invoice_htmlreturnaddress')
-    ? join("\n", $conf->config('invoice_htmlreturnaddress') )
-    : join("\n", map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; }
-                     $conf->config('invoice_latexreturnaddress')
-          );
-  warn $conf->config('invoice_latexreturnaddress');
-  warn $returnaddress;
-
   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),
@@ -1617,15 +1691,18 @@ sub print_html {
     'city'         => encode_entities($cust_main->city),
     'state'        => encode_entities($cust_main->state),
     'zip'          => encode_entities($cust_main->zip),
-#    'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
-#    'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
-    'returnaddress' => $returnaddress,
     'terms'        => $conf->config('invoice_default_terms')
                       || 'Payable upon receipt',
-    #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
+    '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>/; $_; }
+                     $conf->config('invoice_latexreturnaddress')
+          );
+
   my $countrydefault = $conf->config('countrydefault') || 'US';
   if ( $cust_main->country eq $countrydefault ) {
     $invoice_data{'country'} = '';
@@ -1634,8 +1711,20 @@ sub print_html {
       encode_entities(code2country($cust_main->country));
   }
 
-  my $countrydefault = $conf->config('countrydefault') || 'US';
-  $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
+  $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'} =
@@ -1643,11 +1732,13 @@ sub print_html {
 #      map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
 #        $conf->config_orbase('invoice_latexnotes', $suffix)
 #    );
-#
-#  $invoice_data{'footer'} =~ s/\n+$//;
-#  $invoice_data{'smallfooter'} =~ s/\n+$//;
-#  $invoice_data{'notes'} =~ s/\n+$//;
-#
+
+   $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)
index 7dfea50..b7c8b6a 100644 (file)
@@ -158,7 +158,7 @@ sub check {
   if ( $self->plandata =~ /^(agent_)?templatename\s+(.*)$/m ) {
     my $name= $2;
 
-    foreach my $file (qw( template latex latexnotes )) {
+    foreach my $file (qw( template latex latexnotes html htmlnotes )) {
       unless ( $conf->exists("invoice_${file}_$name") ) {
         $conf->set(
           "invoice_${file}_$name" =>
diff --git a/conf/invoice_html b/conf/invoice_html
new file mode 100644 (file)
index 0000000..7b8a85b
--- /dev/null
@@ -0,0 +1,131 @@
+<STYLE TYPE="text/css">
+.invoice { font-family: sans-serif; font-size: 10pt }
+.invoice_header { font-size: 10pt }
+.invoice_headerright TH { border-top: 2px solid #000000; border-bottom: 2px solid #000000 }
+.invoice_headerright TD { font-size: 10pt; empty-cells: show }
+.invoice_longtable table { cellspacing: none }
+.invoice_longtable TH { border-top: 2px solid #000000; border-bottom: 1px solid #000000; padding-left: none; padding-right: none; font-size: 10pt }
+.invoice_desc TD { border-top: 2px solid #000000; font-weight: bold; font-size: 10pt }
+.invoice_extdesc TD { font-size: 8pt }
+.invoice_totaldesc TD { font-size: 10pt; empty-cells: show }
+</STYLE>
+
+<table class="invoice" bgcolor="#ffffff" WIDTH=768 CELLSPACING=8><tr><td>
+
+  <table class="invoice_header" width="100%">
+    <tr>
+     <td><img src="<%= $cid ? "cid:$cid" : '../images/small-logo.png' %>"></td>
+     <td align="left"><%= $returnaddress %></td>
+      <td align="right">
+        <table CLASS="invoice_headerright" cellspacing=0>
+          <tr>
+            <td align="right">
+              Invoice&nbsp;date<BR>
+              <B><%= $date %></B>
+            </td>
+            <td>
+            </td>
+            <td align="left">
+              Invoice&nbsp;number<BR>
+              <B><%= $invnum %></B>
+            </td>
+          </tr>
+          <tr>
+            <th>&nbsp;</th>
+            <th colspan=1 align="center">
+              <FONT SIZE="+3">I</FONT><FONT SIZE="+2">NVOICE</FONT>
+            </th>
+            <th>&nbsp;</th>
+          </tr>
+        </table>
+      </td>
+    </tr>
+
+    <tr>
+      <td>
+      </td>
+      <td align="left">
+        <b><%= $payname %></b><BR>
+        <%= join('<BR>', grep length($_), $company,
+                                          $address1,
+                                          $address2,
+                                          "$city,&nbsp;$state&nbsp;&nbsp;$zip",
+                                          $country,
+                )
+        %>
+      </td>
+      <td align="right">
+        Terms: <%= $terms %><BR>
+        <%= $po_line %>
+      </td>
+    </tr>
+
+  </table>
+
+  <p><b><font size="+1">C</font><font size="+0">HARGES</font></b>
+  <p>
+  <table class="invoice_longtable" CELLSPACING=0 WIDTH="100%">
+    <tr>
+      <th align="center">Ref</th>
+      <th align="left">Description</th>
+      <th align="right">Amount</th>
+    </tr>
+    <%=
+
+      foreach my $line ( @detail_items ) {
+        $OUT .=
+          '<tr class="invoice_desc">'.
+            '<td align="center">'. $line->{'ref'}. '</td>'.
+            '<td align="left">'. $line->{'description'}. '</td>'.
+            '<td align="right">'. $line->{'amount'}. '</td>'.
+          '</tr>'
+        ;
+        foreach my $ext_desc ( @{$line->{'ext_description'} } ) {
+          $OUT .=
+            '<tr class="invoice_extdesc">'.
+              '<td></td>'.
+              '<td align="left">-&nbsp;'. $ext_desc. '</td>'.
+              '<td></td>'.
+            '</tr>'
+        }
+      }
+
+      my $style = 'border-top: 3px solid #000000;';
+      my $linenum = 0;
+
+      foreach my $line ( @total_items ) {
+
+        $style .= 'border-bottom: 3px solid #000000;'
+          if ++$linenum == scalar(@total_items);
+
+        $OUT .=
+          '<tr class="invoice_totaldesc">'.
+            qq(<td style="$style">&nbsp;</td>).
+            qq(<td align="left" style="$style">).
+              $line->{'total_item'}. '</td>'.
+            qq(<td align="right" style="$style">).
+              $line->{'total_amount'}. '</td>'.
+          '</tr>'
+        ;
+
+        $style='';
+
+      }
+
+    %>
+  </table>
+  <br><br>
+
+<!--  <p><b><font size="+1">N</font><font size="+0">OTES</font></b>
+
+  <ol>
+    <li>Please make your check payable to <b>Ivan Kohler</b>
+    <li>If you have any questions please email or telephone.
+  </ol>
+-->
+<%= $notes %>
+
+  <hr NOSHADE SIZE=2 COLOR="#000000">
+  <p align="center"><%= $footer %>
+
+</td></tr></table>
diff --git a/conf/logo.png b/conf/logo.png
new file mode 100644 (file)
index 0000000..1e415e6
Binary files /dev/null and b/conf/logo.png differ
index 7097fda..adaac17 100644 (file)
@@ -14,7 +14,7 @@
       <ul>
         <li>Install teTeX and Ghostscript (included with most distributions).
         <li>Place your logo in EPS (Encapsulated PostScript) format with size 90pt X 36pt (<code>epsffit -c 0 0 90 33 yourlogo.eps &gt;logo.eps</code>) at <code>/usr/local/etc/freeside/conf.<i>your_datasrc</i>/logo.eps</code>.
-        <li>Edit the <b>invoice_latexfooter</b>, <b>invoice_latexnotes</b>, and <b>invoice_latexsmallfooter</b> configuration files.  If you are adventurous, edit <b>invoice_latex</b> as well.
+        <li>Edit the <b>invoice_latexreturnaddress</b>, <b>invoice_latexfooter</b>, <b>invoice_latexnotes</b>, and <b>invoice_latexsmallfooter</b> configuration options.  If you are adventurous, edit <b>invoice_latex</b> as well.
       </ul>
     <li>Plaintext invoice templates
       <ul>
 <!--            <li>$overdue - true if this invoice is overdue -->
           </ul>
       </ul>
+    <li>HTML invoice templates
+      <ul>
+        <li>Place your logo in PNG format at <code>/usr/local/etc/freeside/conf.<i>your_datasrc</i>/logo.png</code>.
+        <li>HTML invoices also use <a href="http://search.cpan.org/~mjd/Text-Template/lib/Text/Template.pm">Text::Template</a>.
+        <li>Edit the <b>invoice_html</b> configuration option.
+        <li>The following configuration options can be set to override the default behaviour of using the invoice_latex* data: <b>invoice_htmlreturnaddress</b>, and <b>invoice_htmlfooter</b>, <b>invoice_htmlnotes</b>.  
+      </ul>
 <!--    <li>Batch credit card processing
       <ul>
         <li>After <a href="man/bin/freeside-daily.html"><b>freeside-daily</b></a> is run, a credit card batch will be in the <a href="schema.html#cust_pay_batch">cust_pay_batch</a> table.  Export this table to your credit card batching.
index c217cc3..5dd8a8d 100755 (executable)
@@ -62,7 +62,7 @@ unless ( $templatename ) {
     ) {
       my $templatename = $1;
       print qq! ( <A HREF="${p}view/cust_bill.cgi?$templatename-$invnum">!.
-            'view text</A> | '.
+            'view</A> | '.
             qq!<A HREF="${p}view/cust_bill-pdf.cgi?$templatename-$invnum.pdf">!.
             'view typeset</A> )';
     }
@@ -74,11 +74,15 @@ unless ( $templatename ) {
   print '</TABLE><BR>';
 }
 
-print '<PRE>', $cust_bill->print_text('', $templatename);
+if ( $conf->exists('invoice_html') ) {
+  print $cust_bill->print_html('', $templatename);
+} else {
+  print '<PRE>', $cust_bill->print_text('', $templatename), '</PRE>';
+}
 
        #formatting
        print <<END;
-    </PRE></FONT>
+    </FONT>
   </BODY>
 </HTML>
 END