X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=8b34cb9187271c94050ffdcd7408eb25bc1147e2;hb=c160bc6f05b17d8084addc4d638ebc908c9ec0e6;hp=ecc6d5f4ce0979c6c38661a07979d9bd98c2618c;hpb=ab4c00e3ac47611b00127418195c93dc095b85f5;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index ecc6d5f4c..8b34cb918 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -1,15 +1,18 @@ 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 IPC::Run3; 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; @@ -18,9 +21,14 @@ use FS::cust_pkg; use FS::cust_credit_bill; use FS::cust_pay_batch; use FS::cust_bill_event; +use FS::part_pkg; +use FS::cust_bill_pay; +use FS::part_bill_event; @ISA = qw( FS::Record ); +$DEBUG = 0; + #ask FS::UID to run this stuff for us later FS::UID->install_callback( sub { $conf = new FS::Conf; @@ -318,6 +326,231 @@ 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 + +use MIME::Entity; + +sub generate_email { + + my $self = shift; + my %args = @_; + + my $me = '[FS::cust_bill::generate_email]'; + + my %return = ( + 'from' => $args{'from'}, + 'subject' => (($args{'subject'}) ? $args{'subject'} : 'Invoice'), + ); + + if (ref($args{'to'} eq 'ARRAY')) { + $return{'to'} = $args{'to'}; + } else { + $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 { + + 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'}) ]; + } + + } + + $alternative->attach( + 'Type' => 'text/plain', + #'Encoding' => 'quoted-printable', + 'Encoding' => '7bit', + 'Data' => $data, + 'Disposition' => 'inline', + ); + + $args{'from'} =~ /\@([\w\.\-]+)/ or $1 = 'example.com'; + my $content_id = join('.', rand()*(2**32), $$, time). "\@$1"; + + my $path = "$FS::UID::conf_dir/conf.$FS::UID::datasrc"; + my $file; + if ( [ -e "$path/logo_". $args{'_template'}. ".png" ] ) { + $file = "$path/logo_". $args{'_template'}. ".png"; + } else { + $file = "$path/logo.png"; + } + + my $image = build MIME::Entity + 'Type' => 'image/png', + 'Encoding' => 'base64', + 'Path' => $file, + 'Filename' => 'logo.png', + 'Content-ID' => "<$content_id>", + ; + + $alternative->attach( + 'Type' => 'text/html', + 'Encoding' => 'quoted-printable', + 'Data' => [ '', + ' ', + ' ', + ' '. 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 { + + 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; + +} + +=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 ] ] ] Sends this invoice to the destinations configured for this customer: send @@ -326,7 +559,8 @@ emails or print. See L. TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. AGENTNUM, if specified, means that this invoice will only be sent for customers -of the specified agent. +of the specified agent or agent(s). AGENTNUM can be a scalar agentnum (for a +single agent) or an arrayref of agentnums. INVOICE_FROM, if specified, overrides the default email invoice From: address. @@ -335,44 +569,127 @@ INVOICE_FROM, if specified, overrides the default email invoice From: address. sub send { my $self = shift; my $template = scalar(@_) ? shift : ''; - return 'N/A' if scalar(@_) && $_[0] && $self->cust_main->agentnum != shift; + if ( scalar(@_) && $_[0] ) { + my $agentnums = ref($_[0]) ? shift : [ shift ]; + return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums; + } + my $invoice_from = scalar(@_) ? shift : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); - my @print_text = $self->print_text('', $template); my @invoicing_list = $self->cust_main->invoicing_list; - if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email + $self->email($template, $invoice_from) + if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list; - #better to notify this person than silence - @invoicing_list = ($invoice_from) unless @invoicing_list; + $self->print($template) + if grep { $_ eq 'POST' } @invoicing_list; #postal - my $error = send_email( - 'from' => $invoice_from, - 'to' => [ grep { $_ ne 'POST' } @invoicing_list ], - 'subject' => 'Invoice', - 'body' => \@print_text, - ); - die "can't email invoice: $error\n" if $error; - #die "$error\n" if $error; + $self->fax($template) + if grep { $_ eq 'FAX' } @invoicing_list; #fax - } + ''; + +} + +=item email [ TEMPLATENAME [ , INVOICE_FROM ] ] + +Emails this invoice. + +TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. + +INVOICE_FROM, if specified, overrides the default email invoice From: address. + +=cut + +sub email { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + my $invoice_from = + scalar(@_) + ? shift + : ( $self->_agent_invoice_from || $conf->config('invoice_from') ); + + my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } + $self->cust_main->invoicing_list; + + #better to notify this person than silence + @invoicing_list = ($invoice_from) unless @invoicing_list; + + my $error = send_email( + $self->generate_email( + 'from' => $invoice_from, + 'to' => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ], + 'template' => $template, + ) + ); + die "can't email invoice: $error\n" if $error; + #die "$error\n" if $error; + +} + +=item lpr_data [ TEMPLATENAME ] + +Returns the postscript or plaintext for this invoice as an arrayref. + +TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. + +=cut + +sub lpr_data { + my( $self, $template) = @_; + $conf->exists('invoice_latex') + ? [ $self->print_ps('', $template) ] + : [ $self->print_text('', $template) ]; +} + +=item print [ TEMPLATENAME ] + +Prints this invoice. + +TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. - if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal - @print_text = $self->print_ps('', $template) - if $conf->config('invoice_latex'); - 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"; +=cut + +sub print { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + my $lpr = $conf->config('lpr'); + + my $outerr = ''; + run3 $lpr, $self->lpr_data($template), \$outerr, \$outerr; + if ( $? ) { + $outerr = ": $outerr" if length($outerr); + die "Error from $lpr (exit status ". ($?>>8). ")$outerr\n"; } - ''; +} + +=item fax [ TEMPLATENAME ] + +Faxes this invoice. + +TEMPLATENAME, if specified, is the name of a suffix for alternate invoices. + +=cut + +sub fax { + my $self = shift; + my $template = scalar(@_) ? shift : ''; + + die 'FAX invoice destination not (yet?) supported with plain text invoices.' + unless $conf->exists('invoice_latex'); + + my $dialstring = $self->cust_main->getfield('fax'); + #Check $dialstring? + + my $error = send_fax( 'docdata' => $self->lpr_data($template), + 'dialstring' => $dialstring, + ); + die $error if $error; } @@ -501,7 +818,7 @@ sub send_csv { ) or die "can't create csv"; print CSV $csv->string. "\n"; - #new charges (false laziness w/print_text) + #new charges (false laziness w/print_text and _items stuff) foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) { my($pkg, $setup, $recur, $sdate, $edate); @@ -681,7 +998,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, @@ -711,7 +1028,9 @@ sub _agent_plandata { 'plan' => 'send_agent', 'plandata' => { 'op' => '~', 'value' => "(^|\n)agentnum ". + '([0-9]*, )*'. $self->cust_main->agentnum. + '(, [0-9]*)*'. "(\n|\$)", }, }, @@ -742,7 +1061,7 @@ L and L for conversion functions. =cut -#still some false laziness w/print_text +#still some false laziness w/_items stuff (and send_csv) sub print_text { my( $self, $today, $template ) = @_; @@ -783,50 +1102,49 @@ sub print_text { ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes ) { - if ( $cust_bill_pkg->pkgnum ) { + my $desc = $cust_bill_pkg->desc; - my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); - my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); - my $pkg = $part_pkg->pkg; + if ( $cust_bill_pkg->pkgnum > 0 ) { if ( $cust_bill_pkg->setup != 0 ) { - my $description = $pkg; + my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; push @buf, [ $description, $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } - $cust_pkg->h_labels($self->_date); + $cust_bill_pkg->cust_pkg->h_labels($self->_date); } if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . - time2str("%x", $cust_bill_pkg->edate) . ")", + "$desc (" . time2str("%x", $cust_bill_pkg->sdate) . " - " . + time2str("%x", $cust_bill_pkg->edate) . ")", $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; push @buf, map { [ " ". $_->[0]. ": ". $_->[1], '' ] } - $cust_pkg->h_labels($cust_bill_pkg->edate, $cust_bill_pkg->sdate); + $cust_bill_pkg->cust_pkg->h_labels( $cust_bill_pkg->edate, + $cust_bill_pkg->sdate ); } push @buf, map { [ " $_", '' ] } $cust_bill_pkg->details; } else { #pkgnum tax or one-shot line item - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; + if ( $cust_bill_pkg->setup != 0 ) { - push @buf, [ $itemdesc, + push @buf, [ $desc, $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ]; } if ( $cust_bill_pkg->recur != 0 ) { - push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - " - . time2str("%x", $cust_bill_pkg->edate). ")", + push @buf, [ "$desc (". time2str("%x", $cust_bill_pkg->sdate). " - " + . time2str("%x", $cust_bill_pkg->edate). ")", $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) ]; } + } + } push @buf,['','-----------']; @@ -935,8 +1253,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 = ( @@ -984,13 +1304,14 @@ L and L for conversion functions. =cut -#still some false laziness w/print_text +#still some false laziness w/print_text (mostly print_text should use _items stuff though) sub print_latex { my( $self, $today, $template ) = @_; $today ||= time; + warn "FS::cust_bill::print_latex called on $self with suffix $template\n" + if $DEBUG; -# my $invnum = $self->invnum; my $cust_main = $self->cust_main; $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; @@ -1000,21 +1321,45 @@ sub print_latex { #my $balance_due = $self->owed + $pr_total - $cr_total; my $balance_due = $self->owed + $pr_total; - #my @collect = (); - #my($description,$amount); - @buf = (); - #create the template $template ||= $self->_agent_template; my $templatefile = 'invoice_latex'; 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 ( length($conf->config_orbase('invoice_latexreturnaddress', $template)) ) { + $returnaddress = join("\n", + $conf->config_orbase('invoice_latexreturnaddress', $template) + ); + } else { + $returnaddress = '~'; + } + my %invoice_data = ( 'invnum' => $self->invnum, 'date' => time2str('%b %o, %Y', $self->_date), + 'today' => time2str('%b %o, %Y', $today), 'agent' => _latex_escape($cust_main->agent->agent), 'payname' => _latex_escape($cust_main->payname), 'company' => _latex_escape($cust_main->company), @@ -1023,23 +1368,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') ), + 'footer' => join("\n", $conf->config_orbase('invoice_latexfooter', $template) ), + 'smallfooter' => join("\n", $conf->config_orbase('invoice_latexsmallfooter', $template) ), + 'returnaddress' => $returnaddress, 'quantity' => 1, 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + '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) +# #do variable substitutions in notes +# map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + $conf->config_orbase('invoice_latexnotes', $template) ); + warn "invoice notes: ". $invoice_data{'notes'}. "\n" + if $DEBUG; $invoice_data{'footer'} =~ s/\n+$//; $invoice_data{'smallfooter'} =~ s/\n+$//; @@ -1050,110 +1402,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; @@ -1162,7 +1604,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; @@ -1241,9 +1689,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( @@ -1269,6 +1717,194 @@ sub print_pdf { } +=item print_html [ TIME [ , TEMPLATE [ , CID ] ] ] + +Returns an HTML invoice, as a scalar. + +TIME an optional value used to control the printing of overdue messages. The +default is now. It isn't the date of the invoice; that's the `_date' field. +It is specified as a UNIX timestamp; see L. Also see +L and L for conversion functions. + +CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used +when emailing the invoice as part of a multipart/related MIME email. + +=cut + +sub print_html { + my( $self, $today, $template, $cid ) = @_; + $today ||= time; + + my $cust_main = $self->cust_main; + $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') ) + unless $cust_main->payname && $cust_main->payby !~ /^(CHEK|DCHK)$/; + + $template ||= $self->_agent_template; + my $templatefile = 'invoice_html'; + my $suffix = length($template) ? "_$template" : ''; + $templatefile .= $suffix; + my @html_template = map "$_\n", $conf->config($templatefile) + or die "cannot load config file $templatefile"; + + my $html_template = new Text::Template( + TYPE => 'ARRAY', + SOURCE => \@html_template, + DELIMITERS => [ '<%=', '%>' ], + ); + + $html_template->compile() + or die 'While compiling ' . $templatefile . ': ' . $Text::Template::ERROR; + + my %invoice_data = ( + 'invnum' => $self->invnum, + 'date' => time2str('%b %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), + 'address1' => encode_entities($cust_main->address1), + 'address2' => encode_entities($cust_main->address2), + 'city' => encode_entities($cust_main->city), + 'state' => encode_entities($cust_main->state), + 'zip' => encode_entities($cust_main->zip), + 'terms' => $conf->config('invoice_default_terms') + || 'Payable upon receipt', + 'cid' => $cid, + 'template' => $template, +# 'conf_dir' => "$FS::UID::conf_dir/conf.$FS::UID::datasrc", + ); + + $invoice_data{'returnaddress'} = + length( $conf->config_orbase('invoice_htmlreturnaddress', $template) ) + ? join("\n", $conf->config('invoice_htmlreturnaddress', $template) ) + : join("\n", map { + s/~/ /g; + s/\\\\\*?\s*$/
/; + s/\\hyphenation\{[\w\s\-]+\}//; + $_; + } + $conf->config_orbase('invoice_latexreturnaddress', $template) + ); + + my $countrydefault = $conf->config('countrydefault') || 'US'; + if ( $cust_main->country eq $countrydefault ) { + $invoice_data{'country'} = ''; + } else { + $invoice_data{'country'} = + encode_entities(code2country($cust_main->country)); + } + + $invoice_data{'notes'} = + length($conf->config_orbase('invoice_htmlnotes', $template)) + ? join("\n", $conf->config_orbase('invoice_htmlnotes', $template) ) + : join("\n", map { + s/%%(.*)$//; + 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'} = +# join("\n", +# map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } +# $conf->config_orbase('invoice_latexnotes', $suffix) +# ); + + $invoice_data{'footer'} = + length($conf->config_orbase('invoice_htmlfooter', $template)) + ? join("\n", $conf->config_orbase('invoice_htmlfooter', $template) ) + : join("\n", map { s/~/ /g; s/\\\\\*?\s*$/
    /; $_; } + $conf->config_orbase('invoice_latexfooter', $template) + ); + + $invoice_data{'po_line'} = + ( $cust_main->payby eq 'BILL' && $cust_main->payinfo ) + ? encode_entities("Purchase Order #". $cust_main->payinfo) + : ''; + + my $money_char = $conf->config('money_char') || '$'; + + foreach my $line_item ( $self->_items ) { + my $detail = { + ext_description => [], + }; + $detail->{'ref'} = $line_item->{'pkgnum'}; + $detail->{'description'} = encode_entities($line_item->{'description'}); + if ( exists $line_item->{'ext_description'} ) { + @{$detail->{'ext_description'}} = map { + encode_entities($_); + } @{$line_item->{'ext_description'}}; + } + $detail->{'amount'} = $money_char. $line_item->{'amount'}; + $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A'; + + push @{$invoice_data{'detail_items'}}, $detail; + } + + + my $taxtotal = 0; + foreach my $tax ( $self->_items_tax ) { + my $total = {}; + $total->{'total_item'} = encode_entities($tax->{'description'}); + $taxtotal += $tax->{'amount'}; + $total->{'total_amount'} = $money_char. $tax->{'amount'}; + push @{$invoice_data{'total_items'}}, $total; + } + + if ( $taxtotal ) { + my $total = {}; + $total->{'total_item'} = 'Sub-total'; + $total->{'total_amount'} = + $money_char. sprintf('%.2f', $self->charged - $taxtotal ); + unshift @{$invoice_data{'total_items'}}, $total; + } + + my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance + { + my $total = {}; + $total->{'total_item'} = '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 @@ -1281,6 +1917,7 @@ sub print_pdf { sub _latex_escape { my $value = shift; $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge; + $value =~ s/([<>])/\$$1\$/g; $value; } @@ -1323,7 +1960,7 @@ sub _items_previous { ' ('. time2str('%x',$_->_date). ')', #'pkgpart' => 'N/A', 'pkgnum' => 'N/A', - 'amount' => sprintf("%10.2f", $_->owed), + 'amount' => sprintf("%.2f", $_->owed), }; } @b; @@ -1361,58 +1998,54 @@ sub _items_cust_bill_pkg { my @b = (); foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { - if ( $cust_bill_pkg->pkgnum ) { + my $desc = $cust_bill_pkg->desc; - my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } ); - my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } ); - my $pkg = $part_pkg->pkg; + if ( $cust_bill_pkg->pkgnum > 0 ) { if ( $cust_bill_pkg->setup != 0 ) { - my $description = $pkg; + my $description = $desc; $description .= ' Setup' if $cust_bill_pkg->recur != 0; - my @d = $cust_pkg->h_labels_short($self->_date); + my @d = $cust_bill_pkg->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), + pkgnum => $cust_bill_pkg->pkgnum, + amount => sprintf("%.2f", $cust_bill_pkg->setup), ext_description => \@d, }; } if ( $cust_bill_pkg->recur != 0 ) { push @b, { - description => "$pkg (" . + description => "$desc (" . 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 => [ $cust_pkg->h_labels_short($cust_bill_pkg->edate, - $cust_bill_pkg->sdate), - $cust_bill_pkg->details, - ], + pkgnum => $cust_bill_pkg->pkgnum, + amount => sprintf("%.2f", $cust_bill_pkg->recur), + ext_description => + [ $cust_bill_pkg->cust_pkg->h_labels_short( $cust_bill_pkg->edate, + $cust_bill_pkg->sdate), + $cust_bill_pkg->details, + ], }; } } else { #pkgnum tax or one-shot line item (??) - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; if ( $cust_bill_pkg->setup != 0 ) { push @b, { - 'description' => $itemdesc, - 'amount' => sprintf("%10.2f", $cust_bill_pkg->setup), + 'description' => $desc, + 'amount' => sprintf("%.2f", $cust_bill_pkg->setup), }; } if ( $cust_bill_pkg->recur != 0 ) { push @b, { - 'description' => "$itemdesc (". + 'description' => "$desc (". 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), }; } @@ -1443,7 +2076,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 ) { @@ -1469,7 +2102,7 @@ sub _items_payments { push @b, { 'description' => "Payment received ". time2str("%x",$_->cust_pay->_date ), - 'amount' => sprintf("%10.2f", $_->amount ) + 'amount' => sprintf("%.2f", $_->amount ) }; } @@ -1479,6 +2112,122 @@ sub _items_payments { =back +=head1 SUBROUTINES + +=over 4 + +=item reprint + +=cut + +sub process_reprint { + process_re_X('print', @_); +} + +=item reemail + +=cut + +sub process_reemail { + process_re_X('email', @_); +} + +=item refax + +=cut + +sub process_refax { + process_re_X('fax', @_); +} + +use Storable qw(thaw); +use Data::Dumper; +use MIME::Base64; +sub process_re_X { + my( $method, $job ) = ( shift, shift ); + + my $param = thaw(decode_base64(shift)); + warn Dumper($param) if $DEBUG; + + re_X( + $method, + $job, + %$param, + ); + +} + +sub re_X { + my($method, $job, %param ) = @_; +# [ 'begin', 'end', 'agentnum', 'open', 'days', 'newest_percust' ], + + #some false laziness w/search/cust_bill.html + my $distinct = ''; + my $orderby = 'ORDER BY cust_bill._date'; + + my @where; + + if ( $param{'begin'} =~ /^(\d+)$/ ) { + push @where, "cust_bill._date >= $1"; + } + if ( $param{'end'} =~ /^(\d+)$/ ) { + push @where, "cust_bill._date < $1"; + } + if ( $param{'agentnum'} =~ /^(\d+)$/ ) { + push @where, "cust_main.agentnum = $1"; + } + + my $owed = + "charged - ( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay + WHERE cust_bill_pay.invnum = cust_bill.invnum ) + - ( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill + WHERE cust_credit_bill.invnum = cust_bill.invnum )"; + + push @where, "0 != $owed" + if $param{'open'}; + + push @where, "cust_bill._date < ". (time-86400*$param{'days'}) + if $param{'days'}; + + my $extra_sql = scalar(@where) ? 'WHERE '. join(' AND ', @where) : ''; + + my $addl_from = 'left join cust_main using ( custnum )'; + + if ( $param{'newest_percust'} ) { + $distinct = 'DISTINCT ON ( cust_bill.custnum )'; + $orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC'; + #$count_query = "SELECT COUNT(DISTINCT cust_bill.custnum), 'N/A', 'N/A'"; + } + + my @cust_bill = qsearch( 'cust_bill', + {}, + "$distinct cust_bill.*", + $extra_sql, + '', + $addl_from + ); + + my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo + foreach my $cust_bill ( @cust_bill ) { + $cust_bill->$method(); + + if ( $job ) { #progressbar foo + $num++; + if ( time - $min_sec > $last ) { + my $error = $job->update_statustext( + int( 100 * $num / scalar(@cust_bill) ) + ); + die $error if $error; + $last = time; + } + } + + } + +} + +=back + =head1 BUGS The delete method.