diff options
-rw-r--r-- | ANNOUNCE.1.5 | 2 | ||||
-rw-r--r-- | FS/FS/Conf.pm | 56 | ||||
-rw-r--r-- | FS/FS/Misc.pm | 116 | ||||
-rw-r--r-- | FS/FS/cust_bill.pm | 357 | ||||
-rw-r--r-- | FS/FS/part_bill_event.pm | 2 | ||||
-rw-r--r-- | conf/invoice_html | 131 | ||||
-rw-r--r-- | conf/logo.png | bin | 0 -> 4887 bytes | |||
-rw-r--r-- | httemplate/docs/billing.html | 9 | ||||
-rwxr-xr-x | httemplate/view/cust_bill.cgi | 10 |
9 files changed, 516 insertions, 167 deletions
diff --git a/ANNOUNCE.1.5 b/ANNOUNCE.1.5 index 7ffd8bd0c..ce3d99dba 100644 --- a/ANNOUNCE.1.5 +++ b/ANNOUNCE.1.5 @@ -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 diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index be282971e..15ac23d53 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -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', }, diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index 7d7b7d061..92780f707 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -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'); diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index fe5a653c0..f62f11268 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -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/~/ /g; s/\\\\\*?\s*$/<BR>/; } - $conf->config('invoice_latexreturnaddress') - ); - warn $conf->config('invoice_latexreturnaddress'); - warn $returnaddress; - my %invoice_data = ( 'invnum' => $self->invnum, 'date' => time2str('%b %o, %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/~/ /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/~/ /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) diff --git a/FS/FS/part_bill_event.pm b/FS/FS/part_bill_event.pm index 7dfea5066..b7c8b6a2d 100644 --- a/FS/FS/part_bill_event.pm +++ b/FS/FS/part_bill_event.pm @@ -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 index 000000000..7b8a85bcc --- /dev/null +++ b/conf/invoice_html @@ -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 date<BR> + <B><%= $date %></B> + </td> + <td> + </td> + <td align="left"> + Invoice number<BR> + <B><%= $invnum %></B> + </td> + </tr> + <tr> + <th> </th> + <th colspan=1 align="center"> + <FONT SIZE="+3">I</FONT><FONT SIZE="+2">NVOICE</FONT> + </th> + <th> </th> + </tr> + </table> + </td> + </tr> + + <tr> + <td> + </td> + <td align="left"> + <b><%= $payname %></b><BR> + <%= join('<BR>', grep length($_), $company, + $address1, + $address2, + "$city, $state $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">- '. $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"> </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 Binary files differnew file mode 100644 index 000000000..1e415e6d8 --- /dev/null +++ b/conf/logo.png diff --git a/httemplate/docs/billing.html b/httemplate/docs/billing.html index 7097fda24..adaac17dc 100644 --- a/httemplate/docs/billing.html +++ b/httemplate/docs/billing.html @@ -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 >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> @@ -30,6 +30,13 @@ <!-- <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. diff --git a/httemplate/view/cust_bill.cgi b/httemplate/view/cust_bill.cgi index c217cc389..5dd8a8d71 100755 --- a/httemplate/view/cust_bill.cgi +++ b/httemplate/view/cust_bill.cgi @@ -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 |