X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=fe5a653c068deb7653bd2b84d36036d03e7bc60a;hb=d33c75b60d9cb9f7155635dc2cd25307f06d947f;hp=f31e455ba7dc5971009de7b79a2b5f56a15ebeed;hpb=a78922d1673c7f79658d3b51353b4f63102bc7c8;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index f31e455ba..fe5a653c0 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,15 +1,17 @@ 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; +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 ); +use FS::Misc qw( send_email send_fax ); use FS::cust_main; use FS::cust_bill_pkg; use FS::cust_credit; @@ -21,6 +23,8 @@ use FS::cust_bill_event; @ISA = qw( FS::Record ); +$DEBUG = 1; + #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { $conf = new FS::Conf; @@ -318,6 +322,80 @@ sub owed { $balance; } + +=item generate_email PARAMHASH + +PARAMHASH can contain the following: + +=over 4 + +=item from => sender address, required + +=item tempate => alternate template name, optional + +=item print_text => text attachment arrayref, optional + +=item subject => email subject, optional + +=back + +Returns an argument list to be passed to L. + +=cut + +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 $email_text; + if ($conf->exists('invoice_email_pdf') + and scalar($conf->config('invoice_email_pdf_note'))) { + + #warn "[FS::cust_bill::send] using 'invoice_email_pdf_note'"; + $email_text = [ map { $_ . "\n" } $conf->config('invoice_email_pdf_note') ]; + } else { + #warn "[FS::cust_bill::send] not using 'invoice_email_pdf_note'"; + if (ref($args{'print_text'}) eq 'ARRAY') { + $email_text = $args{'print_text'}; + } else { + $email_text = [ $self->print_text('', $args{'template'}) ]; + } + } + + my @invoicing_list; + if (ref($args{'to'} eq 'ARRAY')) { + @invoicing_list = @{$args{'to'}}; + } else { + @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; + } + + return ( + 'from' => $args{'from'}, + 'to' => [ @invoicing_list ], + 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), + 'body' => $email_text, + 'mimeparts' => $mimeparts, + ); + + +} + =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] Sends this invoice to the destinations configured for this customer: send @@ -344,39 +422,80 @@ sub send { my @print_text = $self->print_text('', $template); my @invoicing_list = $self->cust_main->invoicing_list; - if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email + if ( grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list ) { + #email #better to notify this person than silence @invoicing_list = ($invoice_from) unless @invoicing_list; my $error = send_email( - 'from' => $invoice_from, - 'to' => [ grep { $_ ne 'POST' } @invoicing_list ], - 'subject' => 'Invoice', - 'body' => \@print_text, + $self->generate_email( + 'from' => $invoice_from, + 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ], + 'print_text' => [ @print_text ], + ) ); die "can't email invoice: $error\n" if $error; + #die "$error\n" if $error; } - if ( $conf->config('invoice_latex') ) { - @print_text = $self->print_ps('', $template); - } + if ( grep { $_ =~ /^(POST|FAX)$/ } @invoicing_list ) { + my $lpr_data; + if ($conf->config('invoice_latex')) { + $lpr_data = [ $self->print_ps('', $template) ]; + } else { + $lpr_data = \@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"; + } + + if ( grep { $_ eq 'FAX' } @invoicing_list ) { #fax + die 'FAX invoice destination not supported with plain text invoices.' + unless $conf->exists('invoice_latex'); + my $dialstring = $self->cust_main->getfield('fax'); + #Check $dialstring? + my $error = send_fax(docdata => $lpr_data, dialstring => $dialstring); + die $error if $error; + } - if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal - my $lpr = $conf->config('lpr'); - open(LPR, "|$lpr") - or die "Can't open pipe to $lpr: $!\n"; - print LPR @print_text; - close LPR - or die $! ? "Error closing $lpr: $!\n" - : "Exit status $? from $lpr\n"; } ''; } +=item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ] + +Like B, but only sends the invoice if it is the newest open invoice for +this customer. + +=cut + +sub send_if_newest { + my $self = shift; + + return '' + if scalar( + grep { $_->owed > 0 } + qsearch('cust_bill', { + 'custnum' => $self->custnum, + #'_date' => { op=>'>', value=>$self->_date }, + 'invnum' => { op=>'>', value=>$self->invnum }, + } ) + ); + + $self->send(@_); +} + =item send_csv OPTIONS Sends invoice as a CSV data-file to a remote host with the specified protocol. @@ -659,7 +778,7 @@ sub batch_card { 'state' => $cust_main->getfield('state'), 'zip' => $cust_main->getfield('zip'), 'country' => $cust_main->getfield('country'), - 'cardnum' => $cust_main->getfield('payinfo'), + 'cardnum' => $cust_main->payinfo, 'exp' => $cust_main->getfield('paydate'), 'payname' => $cust_main->getfield('payname'), 'amount' => $self->owed, @@ -683,7 +802,7 @@ sub _agent_invoice_from { sub _agent_plandata { my( $self, $option ) = @_; - my $cust_bill_event = qsearchs( 'part_bill_event', + my $part_bill_event = qsearchs( 'part_bill_event', { 'payby' => $self->cust_main->payby, 'plan' => 'send_agent', @@ -697,12 +816,13 @@ sub _agent_plandata { 'ORDER BY seconds LIMIT 1' ); - return '' unless $cust_bill_event; + return '' unless $part_bill_event; - if ( $cust_bill_event->plandata =~ /^$option (.*)$/m ) { + if ( $part_bill_event->plandata =~ /^$option (.*)$/m ) { return $1; } else { - warn "can't parse plandata for $1"; + warn "can't parse part_bill_event eventpart#". $part_bill_event->eventpart. + " plandata for $option"; return ''; } @@ -719,14 +839,16 @@ L and L for conversion functions. =cut +#still some false laziness w/print_text sub print_text { my( $self, $today, $template ) = @_; $today ||= time; + # my $invnum = $self->invnum; - my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } ); + my $cust_main = $self->cust_main; $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) - unless $cust_main->payname && $cust_main->payby ne 'CHEK'; + unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits @@ -770,7 +892,8 @@ sub print_text { push @buf, [ $description, $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; push @buf, - map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; + map { [ " ". $_->[0]. ": ". $_->[1], '' ] } + $cust_pkg->h_labels($self->_date); } if ( $cust_bill_pkg->recur != 0 ) { @@ -780,7 +903,8 @@ sub print_text { $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; push @buf, - map { [ " ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels; + map { [ " ". $_->[0]. ": ". $_->[1], '' ] } + $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate); } push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; @@ -908,8 +1032,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 = ( @@ -962,32 +1088,58 @@ 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 ne 'CHEK'; + unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance # my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - #my @collect = (); - #my($description,$amount); - @buf = (); - #create the template $template ||= $self->_agent_template; my $templatefile = 'invoice_latex'; my $suffix = length($template) ? "_$template" : ''; $templatefile .= $suffix; - my @invoice_template = $conf->config($templatefile) + my @invoice_template = map "$_\n", $conf->config($templatefile) or die "cannot load config file $templatefile"; + my($format, $text_template); + if ( grep { /^%%Detail/ } @invoice_template ) { + #change this to a die when the old code is removed + warn "old-style invoice template $templatefile; ". + "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n"; + $format = 'old'; + } else { + $format = 'Text::Template'; + $text_template = new Text::Template( + TYPE => 'ARRAY', + SOURCE => \@invoice_template, + DELIMITERS => [ '[@--', '--@]' ], + ); + + $text_template->compile() + or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR; + } + + my $returnaddress; + if ( $conf->exists('invoice_latexreturnaddress') + && length($conf->exists('invoice_latexreturnaddress')) + ) + { + $returnaddress = join("\n", $conf->config('invoice_latexreturnaddress') ); + } else { + $returnaddress = '~'; + } + my %invoice_data = ( 'invnum' => $self->invnum, 'date' => time2str('%b %o, %Y', $self->_date), + '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), @@ -996,23 +1148,30 @@ 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' => $returnaddress, 'quantity' => 1, 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", ); my $countrydefault = $conf->config('countrydefault') || 'US'; - $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) + $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+$//; @@ -1023,110 +1182,200 @@ sub print_latex { ? _latex_escape("Purchase Order #". $cust_main->payinfo) : '~'; - my @line_item = (); - my @total_item = (); my @filled_in = (); - while ( @invoice_template ) { - my $line = shift @invoice_template; - - if ( $line =~ /^%%Detail\s*$/ ) { - - while ( ( my $line_item_line = shift @invoice_template ) - !~ /^%%EndDetail\s*$/ ) { - push @line_item, $line_item_line; - } - foreach my $line_item ( $self->_items ) { - #foreach my $line_item ( $self->_items_pkg ) { - $invoice_data{'ref'} = $line_item->{'pkgnum'}; - $invoice_data{'description'} = _latex_escape($line_item->{'description'}); - if ( exists $line_item->{'ext_description'} ) { - $invoice_data{'description'} .= - "\\tabularnewline\n~~". - join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} ); + if ( $format eq 'old' ) { + + my @line_item = (); + my @total_item = (); + while ( @invoice_template ) { + my $line = shift @invoice_template; + + if ( $line =~ /^%%Detail\s*$/ ) { + + while ( ( my $line_item_line = shift @invoice_template ) + !~ /^%%EndDetail\s*$/ ) { + push @line_item, $line_item_line; + } + foreach my $line_item ( $self->_items ) { + #foreach my $line_item ( $self->_items_pkg ) { + $invoice_data{'ref'} = $line_item->{'pkgnum'}; + $invoice_data{'description'} = + _latex_escape($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + $invoice_data{'description'} .= + "\\tabularnewline\n~~". + join( "\\tabularnewline\n~~", + map _latex_escape($_), @{$line_item->{'ext_description'}} + ); + } + $invoice_data{'amount'} = $line_item->{'amount'}; + $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + push @filled_in, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item; + } + + } elsif ( $line =~ /^%%TotalDetails\s*$/ ) { + + while ( ( my $total_item_line = shift @invoice_template ) + !~ /^%%EndTotalDetails\s*$/ ) { + push @total_item, $total_item_line; + } + + my @total_fill = (); + + my $taxtotal = 0; + foreach my $tax ( $self->_items_tax ) { + $invoice_data{'total_item'} = _latex_escape($tax->{'description'}); + $taxtotal += $tax->{'amount'}; + $invoice_data{'total_amount'} = '\dollar '. $tax->{'amount'}; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; } - $invoice_data{'amount'} = $line_item->{'amount'}; - $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; - push @filled_in, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item; - } - - } elsif ( $line =~ /^%%TotalDetails\s*$/ ) { - - while ( ( my $total_item_line = shift @invoice_template ) - !~ /^%%EndTotalDetails\s*$/ ) { - push @total_item, $total_item_line; - } - - my @total_fill = (); - my $taxtotal = 0; - foreach my $tax ( $self->_items_tax ) { - $invoice_data{'total_item'} = _latex_escape($tax->{'description'}); - $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} ); + if ( $taxtotal ) { + $invoice_data{'total_item'} = 'Sub-total'; + $invoice_data{'total_amount'} = + '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); + unshift @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + $invoice_data{'total_item'} = '\textbf{Total}'; + $invoice_data{'total_amount'} = + '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; push @total_fill, map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @total_item; - } - - if ( $taxtotal ) { - $invoice_data{'total_item'} = 'Sub-total'; + + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + foreach my $credit ( $self->_items_credits ) { + $invoice_data{'total_item'} = _latex_escape($credit->{'description'}); + #$credittotal + $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'}; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + # payments + foreach my $payment ( $self->_items_payments ) { + $invoice_data{'total_item'} = _latex_escape($payment->{'description'}); + #$paymenttotal + $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'}; + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; $invoice_data{'total_amount'} = - '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); - unshift @total_fill, + '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; + push @total_fill, map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @total_item; + + push @filled_in, @total_fill; + + } else { + #$line =~ s/\$(\w+)/$invoice_data{$1}/eg; + $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg; + push @filled_in, $line; } + + } - $invoice_data{'total_item'} = '\textbf{Total}'; - $invoice_data{'total_amount'} = - '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - - #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments - - # credits - foreach my $credit ( $self->_items_credits ) { - $invoice_data{'total_item'} = _latex_escape($credit->{'description'}); - #$credittotal - $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'}; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - } + sub nounder { + my $var = $1; + $var =~ s/_/\-/g; + $var; + } - # payments - foreach my $payment ( $self->_items_payments ) { - $invoice_data{'total_item'} = _latex_escape($payment->{'description'}); - #$paymenttotal - $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'}; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; + } elsif ( $format eq 'Text::Template' ) { + + my @detail_items = (); + my @total_items = (); + + $invoice_data{'detail_items'} = \@detail_items; + $invoice_data{'total_items'} = \@total_items; + + foreach my $line_item ( $self->_items ) { + my $detail = { + ext_description => [], + }; + $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'quantity'} = 1; + $detail->{'description'} = _latex_escape($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + @{$detail->{'ext_description'}} = map { + _latex_escape($_); + } @{$line_item->{'ext_description'}}; } - - $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; - $invoice_data{'total_amount'} = + $detail->{'amount'} = $line_item->{'amount'}; + $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + + push @detail_items, $detail; + } + + + my $taxtotal = 0; + foreach my $tax ( $self->_items_tax ) { + my $total = {}; + $total->{'total_item'} = _latex_escape($tax->{'description'}); + $taxtotal += $tax->{'amount'}; + $total->{'total_amount'} = '\dollar '. $tax->{'amount'}; + push @total_items, $total; + } + + if ( $taxtotal ) { + my $total = {}; + $total->{'total_item'} = 'Sub-total'; + $total->{'total_amount'} = + '\dollar '. sprintf('%.2f', $self->charged - $taxtotal ); + unshift @total_items, $total; + } + + { + my $total = {}; + $total->{'total_item'} = '\textbf{Total}'; + $total->{'total_amount'} = + '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}'; + push @total_items, $total; + } + + #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments + + # credits + foreach my $credit ( $self->_items_credits ) { + my $total; + $total->{'total_item'} = _latex_escape($credit->{'description'}); + #$credittotal + $total->{'total_amount'} = '-\dollar '. $credit->{'amount'}; + push @total_items, $total; + } + + # payments + foreach my $payment ( $self->_items_payments ) { + my $total = {}; + $total->{'total_item'} = _latex_escape($payment->{'description'}); + #$paymenttotal + $total->{'total_amount'} = '-\dollar '. $payment->{'amount'}; + push @total_items, $total; + } + + { + my $total; + $total->{'total_item'} = '\textbf{'. $self->balance_due_msg. '}'; + $total->{'total_amount'} = '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}'; - push @total_fill, - map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } - @total_item; - - push @filled_in, @total_fill; - - } else { - #$line =~ s/\$(\w+)/$invoice_data{$1}/eg; - $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg; - push @filled_in, $line; + push @total_items, $total; } - } - - sub nounder { - my $var = $1; - $var =~ s/_/\-/g; - $var; + } else { + die "guru meditation #54"; } my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc; @@ -1135,7 +1384,13 @@ sub print_latex { SUFFIX => '.tex', UNLINK => 0, ) or die "can't open temp file: $!\n"; - print $fh join("\n", @filled_in ), "\n"; + if ( $format eq 'old' ) { + print $fh join('', @filled_in ); + } elsif ( $format eq 'Text::Template' ) { + $text_template->fill_in(OUTPUT => $fh, HASH => \%invoice_data); + } else { + die "guru meditation #32"; + } close $fh; $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename; @@ -1165,12 +1420,12 @@ sub print_ps { my $sfile = shell_quote $file; system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed: $!"; + or die "pslatex $file.tex failed; see $file.log for details?\n"; system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed: $!"; + or die "pslatex $file.tex failed; see $file.log for details?\n"; system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ) == 0 - or die "dvips failed: $!"; + or die "dvips failed"; open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps: $! (error in LaTeX template?)\n"; @@ -1214,9 +1469,9 @@ sub print_pdf { my $sfile = shell_quote $file; system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed: $!"; + or die "pslatex $file.tex failed; see $file.log for details?\n"; system("pslatex $sfile.tex >/dev/null 2>&1") == 0 - or die "pslatex $file.tex failed: $!"; + or die "pslatex $file.tex failed; see $file.log for details?\n"; #system('dvipdf', "$file.dvi", "$file.pdf" ); system( @@ -1242,6 +1497,239 @@ sub print_pdf { } +=item print_html [ TIME [ , TEMPLATE ] ] + +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. Also see +L and L for conversion functions. + +=cut + +#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; +# +#} + +sub print_html { + my( $self, $today, $template ) = @_; + $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 $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), + '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), +# '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') ), +# 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", + ); + + 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)); + } + + my $countrydefault = $conf->config('countrydefault') || 'US'; + $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault; + +# #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'} =~ s/\n+$//; +# $invoice_data{'smallfooter'} =~ s/\n+$//; +# $invoice_data{'notes'} =~ s/\n+$//; +# + $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'} = 'Total'; + $total->{'total_amount'} = + "$money_char". sprintf('%.2f', $self->charged + $pr_total ). ''; + 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'} = ''. $self->balance_due_msg. ''; + $total->{'total_amount'} = + "$money_char". sprintf('%.2f', $self->owed + $pr_total ). ''; + 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 @@ -1253,7 +1741,8 @@ sub print_pdf { sub _latex_escape { my $value = shift; - $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge; + $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge; + $value =~ s/([<>])/\$$1\$/g; $value; } @@ -1296,7 +1785,7 @@ sub _items_previous { ' ('. time2str('%x',$_->_date). ')', #'pkgpart' => 'N/A', 'pkgnum' => 'N/A', - 'amount' => sprintf("%10.2f", $_->owed), + 'amount' => sprintf("%.2f", $_->owed), }; } @b; @@ -1340,45 +1829,32 @@ sub _items_cust_bill_pkg { my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); my $pkg = $part_pkg->pkg; - my %labels; - #tie %labels, 'Tie::IxHash'; - push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels; - my @ext_description; - foreach my $label ( keys %labels ) { - my @values = @{ $labels{$label} }; - my $num = scalar(@values); - if ( $num > 5 ) { - push @ext_description, "$label ($num)"; - } else { - push @ext_description, map { "$label: $_" } @values; - } - } - if ( $cust_bill_pkg->setup != 0 ) { my $description = $pkg; $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = @ext_description; + my @d = $cust_pkg->h_labels_short($self->_date); push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0; push @b, { - 'description' => $description, - #'pkgpart' => $part_pkg->pkgpart, - 'pkgnum' => $cust_pkg->pkgnum, - 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup), - 'ext_description' => \@d, + description => $description, + #pkgpart => $part_pkg->pkgpart, + pkgnum => $cust_pkg->pkgnum, + amount => sprintf("%.2f", $cust_bill_pkg->setup), + ext_description => \@d, }; } if ( $cust_bill_pkg->recur != 0 ) { push @b, { - 'description' => "$pkg (" . + description => "$pkg (" . time2str('%x', $cust_bill_pkg->sdate). ' - '. time2str('%x', $cust_bill_pkg->edate). ')', - #'pkgpart' => $part_pkg->pkgpart, - 'pkgnum' => $cust_pkg->pkgnum, - 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur), - 'ext_description' => [ @ext_description, - $cust_bill_pkg->details, - ], + #pkgpart => $part_pkg->pkgpart, + pkgnum => $cust_pkg->pkgnum, + amount => sprintf("%.2f", $cust_bill_pkg->recur), + ext_description => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate, + $cust_bill_pkg->sdate), + $cust_bill_pkg->details, + ], }; } @@ -1390,7 +1866,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 ) { @@ -1398,7 +1874,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), }; } @@ -1429,7 +1905,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 ) { @@ -1455,7 +1931,7 @@ sub _items_payments { push @b, { 'description' => "Payment received ". time2str("%x",$_->cust_pay->_date ), - 'amount' => sprintf("%10.2f", $_->amount ) + 'amount' => sprintf("%.2f", $_->amount ) }; }