From: ivan Date: Sat, 14 May 2005 16:27:26 +0000 (+0000) Subject: html invoices! X-Git-Tag: BEFORE_FINAL_MASONIZE~538 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=3081639bd119c6d281ef23139649b2e73ba62754 html invoices! 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!) --- 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 billing documentation 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 billing documentation 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 billing documentation 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 billing documentation 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 - (required) comma-separated scalar or arrayref of recipients I - (required) -I - (optional) MIME type +I - (optional) MIME type for the body + +I - (required unless I is true) arrayref of body text lines + +I - (optional, but required if I is true) arrayref of MIME::Entity->build PARAMHASH refs or MIME::Entity objects. These will be passed as arguments to MIME::Entity->attach(). + +I - (optional) when set true, send_email will ignore the I option and simply construct a message with the given I. In this case, +I, if specified, overrides the default "multipart/mixed" for the outermost MIME container. -I - (required) arrayref of body text lines +I - (optional) when using nobody, optional top-level MIME +encoding which, if specified, overrides the default "7bit". -I - (optional) arrayref of MIME::Entity->build PARAMHASH refs, not MIME::Entity objects. These will be passed as arguments to MIME::Entity->attach(). +I - (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. =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' => [ '', + ' ', + ' ', + ' '. encode_entities($return{'subject'}), + ' ', + ' ', + ' ', + $self->print_html('', $args{'template'}, $content_id), + ' ', + '', + ], + '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. Also see L and L 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 () { -# -# s///; -## s/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 .= $_; -# } -# -# 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*$/
/; } - $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*$/
/; $_; } + $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/%%(.*)$//; + s/\\section\*\{\\textsc\{(.)(.*)\}\}/

$1<\/font>\U$2<\/b>/; + s/\\begin\{enumerate\}/

    /; + s/\\item /
  1. /; + s/\\end\{enumerate\}/<\/ol>/; + s/\\textbf\{(.*)\}/$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*$/
    /; $_; } + $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 @@ + + +
    + + + + + + + + + + + + + + +
    "><%= $returnaddress %> + + + + + + + + + + + +
    + Invoice date
    + <%= $date %> +
    + + Invoice number
    + <%= $invnum %> +
      + INVOICE +  
    +
    + + <%= $payname %>
    + <%= join('
    ', grep length($_), $company, + $address1, + $address2, + "$city, $state  $zip", + $country, + ) + %> +
    + Terms: <%= $terms %>
    + <%= $po_line %> +
    + +

    CHARGES +

    + + + + + + + <%= + + foreach my $line ( @detail_items ) { + $OUT .= + ''. + ''. + ''. + ''. + '' + ; + foreach my $ext_desc ( @{$line->{'ext_description'} } ) { + $OUT .= + ''. + ''. + ''. + ''. + '' + } + } + + 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 .= + ''. + qq(). + qq('. + qq('. + '' + ; + + $style=''; + + } + + %> +
    RefDescriptionAmount
    '. $line->{'ref'}. ''. $line->{'description'}. ''. $line->{'amount'}. '
    - '. $ext_desc. '
     ). + $line->{'total_item'}. '). + $line->{'total_amount'}. '
    +

    + + +<%= $notes %> + +


    +

    <%= $footer %> + +

    diff --git a/conf/logo.png b/conf/logo.png new file mode 100644 index 000000000..1e415e6d8 Binary files /dev/null and b/conf/logo.png differ 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 @@
    • Install teTeX and Ghostscript (included with most distributions).
    • Place your logo in EPS (Encapsulated PostScript) format with size 90pt X 36pt (epsffit -c 0 0 90 33 yourlogo.eps >logo.eps) at /usr/local/etc/freeside/conf.your_datasrc/logo.eps. -
    • Edit the invoice_latexfooter, invoice_latexnotes, and invoice_latexsmallfooter configuration files. If you are adventurous, edit invoice_latex as well. +
    • Edit the invoice_latexreturnaddress, invoice_latexfooter, invoice_latexnotes, and invoice_latexsmallfooter configuration options. If you are adventurous, edit invoice_latex as well.
  2. Plaintext invoice templates
      @@ -30,6 +30,13 @@
    +
  3. HTML invoice templates +
      +
    • Place your logo in PNG format at /usr/local/etc/freeside/conf.your_datasrc/logo.png. +
    • HTML invoices also use Text::Template. +
    • Edit the invoice_html configuration option. +
    • The following configuration options can be set to override the default behaviour of using the invoice_latex* data: invoice_htmlreturnaddress, and invoice_htmlfooter, invoice_htmlnotes. +