X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=1c3941b21e39b282110946c12932c587c1298b2c;hb=b8049903c69d96cf7b810ab16235789ef8a86fba;hp=54641ee358ee97b8d919c60e927cd516ef507424;hpb=88d237b8fe396b4e85a16e20eccfeb4e22e94e24;p=freeside.git diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 54641ee35..1c3941b21 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -3,10 +3,12 @@ package FS::cust_bill; use strict; use vars qw( @ISA $conf $money_char ); use vars qw( $lpr $invoice_from $smtpmachine ); -use vars qw( $processor ); +use vars qw( $cybercash ); use vars qw( $xaction $E_NoErr ); use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options ); +use vars qw( $ach_processor $ach_login $ach_password $ach_action @ach_options ); use vars qw( $invoice_lines @buf ); #yuck +use vars qw( $realtime_bop_decline_quiet ); use Date::Format; use Mail::Internet 1.44; use Mail::Header; @@ -24,6 +26,8 @@ use FS::cust_bill_event; @ISA = qw( FS::Record ); +$realtime_bop_decline_quiet = 0; + #ask FS::UID to run this stuff for us later $FS::UID::callback{'FS::cust_bill'} = sub { @@ -35,7 +39,33 @@ $FS::UID::callback{'FS::cust_bill'} = sub { $invoice_from = $conf->config('invoice_from'); $smtpmachine = $conf->config('smtpmachine'); - if ( $conf->exists('business-onlinepayment') ) { + ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', ''); + @bop_options = (); + ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', ''); + @ach_options = (); + + if ( $conf->exists('cybercash3.2') ) { + require CCMckLib3_2; + #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2); + require CCMckDirectLib3_2; + #qw(SendCC2_1Server); + require CCMckErrno3_2; + #qw(MCKGetErrorMessage $E_NoErr); + import CCMckErrno3_2 qw($E_NoErr); + + my $merchant_conf; + ($merchant_conf,$xaction)= $conf->config('cybercash3.2'); + my $status = &CCMckLib3_2::InitConfig($merchant_conf); + if ( $status != $E_NoErr ) { + warn "CCMckLib3_2::InitConfig error:\n"; + foreach my $key (keys %CCMckLib3_2::Config) { + warn " $key => $CCMckLib3_2::Config{$key}\n" + } + my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status); + die "CCMckLib3_2::InitConfig fatal error: $errmsg\n"; + } + $cybercash='cybercash3.2'; + } elsif ( $conf->exists('business-onlinepayment') ) { ( $bop_processor, $bop_login, $bop_password, @@ -43,8 +73,20 @@ $FS::UID::callback{'FS::cust_bill'} = sub { @bop_options ) = $conf->config('business-onlinepayment'); $bop_action ||= 'normal authorization'; + ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) = + ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options ); + eval "use Business::OnlinePayment"; + } + + if ( $conf->exists('business-onlinepayment-ach') ) { + ( $ach_processor, + $ach_login, + $ach_password, + $ach_action, + @ach_options + ) = $conf->config('business-onlinepayment-ach'); + $ach_action ||= 'normal authorization'; eval "use Business::OnlinePayment"; - $processor="Business::OnlinePayment::$bop_processor"; } }; @@ -352,7 +394,7 @@ 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 { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email #better to notify this person than silence @invoicing_list = ($invoice_from) unless @invoicing_list; @@ -381,6 +423,10 @@ sub send { } + if ( $conf->config('invoice_latex') ) { + @print_text = $self->print_ps('', $template); + } + if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal open(LPR, "|$lpr") or return "Can't open pipe to $lpr: $!"; @@ -514,13 +560,10 @@ sub send_csv { time2str("%x", $cust_bill_pkg->edate), ); - } else { #pkgnum tax + } else { #pkgnum Tax next unless $cust_bill_pkg->setup != 0; - my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') - ? ( $cust_bill_pkg->itemdesc || 'Tax' ) - : 'Tax'; ($pkg, $setup, $recur, $sdate, $edate) = - ( $itemdesc, sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); + ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' ); } $csv->combine( @@ -592,7 +635,15 @@ for supported processors. sub realtime_card { my $self = shift; - $self->realtime_bop('CC', @_); + $self->realtime_bop( + 'CC', + $bop_processor, + $bop_login, + $bop_password, + $bop_action, + \@bop_options, + @_ + ); } =item realtime_ach @@ -606,25 +657,56 @@ for supported processors. sub realtime_ach { my $self = shift; - $self->realtime_bop('CHECK', @_); + $self->realtime_bop( + 'ECHECK', + $ach_processor, + $ach_login, + $ach_password, + $ach_action, + \@ach_options, + @_ + ); } -sub realtime_bop { +=item realtime_lec + +Attempts to pay this invoice with phone bill (LEC) payment via a +Business::OnlinePayment realtime gateway. See +http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment +for supported processors. + +=cut + +sub realtime_lec { my $self = shift; - my $method = shift; - my $cust_main = $self->cust_main; - my $amount = $self->owed; + $self->realtime_bop( + 'LEC', + $bop_processor, + $bop_login, + $bop_password, + $bop_action, + \@bop_options, + @_ + ); +} - unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) { - return "Real-time card/ACH processing not enabled (processor $processor)"; - } - my $bop_processor = $1; #hmm? +sub realtime_bop { + my( $self, $method, $processor, $login, $password, $action, $options ) = @_; + + #trim an extraneous blank line + pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/; + + my $cust_main = $self->cust_main; + my $balance = $cust_main->balance; + my $amount = ( $balance < $self->owed ) ? $balance : $self->owed; + $amount = sprintf("%.2f", $amount); + return "not run (balance $balance)" unless $amount > 0; my $address = $cust_main->address1; $address .= ", ". $cust_main->address2 if $cust_main->address2; my($payname, $payfirst, $paylast); - if ( $cust_main->payname && $method ne 'CHECK' ) { + if ( $cust_main->payname && $method ne 'ECHECK' ) { $payname = $cust_main->payname; $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ or do { @@ -645,7 +727,7 @@ sub realtime_bop { } my $email = $invoicing_list[0]; - my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action ); + my( $action1, $action2 ) = split(/\s*\,\s*/, $action ); my $description = 'Internet Services'; if ( $conf->exists('business-onlinepayment-description') ) { @@ -665,23 +747,39 @@ sub realtime_bop { my %content; if ( $method eq 'CC' ) { + $content{card_number} = $cust_main->payinfo; $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; $content{expiration} = "$2/$1"; - } elsif ( $method eq 'CHECK' ) { + + $content{cvv2} = $cust_main->paycvv + if defined $cust_main->dbdef_table->column('paycvv') + && length($cust_main->paycvv); + + $content{recurring_billing} = 'YES' + if qsearch('cust_pay', { 'custnum' => $cust_main->custnum, + 'payby' => 'CARD', + 'payinfo' => $cust_main->payinfo, } ); + + } elsif ( $method eq 'ECHECK' ) { my($account_number,$routing_code) = $cust_main->payinfo; ( $content{account_number}, $content{routing_code} ) = split('@', $cust_main->payinfo); $content{bank_name} = $cust_main->payname; + $content{account_type} = 'CHECKING'; + $content{account_name} = $payname; + $content{customer_org} = $self->company ? 'B' : 'I'; + $content{customer_ssn} = $self->ss; + } elsif ( $method eq 'LEC' ) { + $content{phone} = $cust_main->payinfo; } my $transaction = - new Business::OnlinePayment( $bop_processor, @bop_options ); + new Business::OnlinePayment( $processor, @$options ); $transaction->content( - %content, 'type' => $method, - 'login' => $bop_login, - 'password' => $bop_password, + 'login' => $login, + 'password' => $password, 'action' => $action1, 'description' => $description, 'amount' => $amount, @@ -698,6 +796,7 @@ sub realtime_bop { 'referer' => 'http://cleanwhisker.420.am/', 'email' => $email, 'phone' => $cust_main->daytime || $cust_main->night, + %content, #after ); $transaction->submit(); @@ -710,14 +809,14 @@ sub realtime_bop { #warn "********* $auth ***********\n"; #warn "********* $ordernum ***********\n"; my $capture = - new Business::OnlinePayment( $bop_processor, @bop_options ); + new Business::OnlinePayment( $processor, @$options ); my %capture = ( %content, type => $method, action => $action2, - login => $bop_login, - password => $bop_password, + login => $login, + password => $password, order_number => $ordernum, amount => $amount, authorization => $auth, @@ -744,18 +843,37 @@ sub realtime_bop { } + #remove paycvv after initial transaction + #make this disable-able via a config option if anyone insists? + # (though that probably violates cardholder agreements) + use Business::CreditCard; + if ( defined $cust_main->dbdef_table->column('paycvv') + && length($cust_main->paycvv) + && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save') + + ) { + my $new = new FS::cust_main { $cust_main->hash }; + $new->paycvv(''); + my $error = $new->replace($cust_main); + if ( $error ) { + warn "error removing cvv: $error\n"; + } + } + + #result handling if ( $transaction->is_success() ) { my %method2payby = ( - 'CC' => 'CARD', - 'CHECK' => 'CHEK', + 'CC' => 'CARD', + 'ECHECK' => 'CHEK', + 'LEC' => 'LECB', ); my $cust_pay = new FS::cust_pay ( { 'invnum' => $self->invnum, 'paid' => $amount, '_date' => '', - 'payby' => method2payby{$method}, + 'payby' => $method2payby{$method}, 'payinfo' => $cust_main->payinfo, 'paybatch' => "$processor:". $transaction->authorization, } ); @@ -776,8 +894,10 @@ sub realtime_bop { my $perror = "$processor error, invnum #". $self->invnum. ': '. $transaction->result_code. ": ". $transaction->error_message; - if ( $conf->exists('emaildecline') + if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline') && grep { $_ ne 'POST' } $cust_main->invoicing_list + && ! grep { $_ eq $transaction->error_message } + $conf->config('emaildecline-exclude') ) { my @templ = $conf->config('declinetemplate'); my $template = new Text::Template ( @@ -817,6 +937,84 @@ sub realtime_bop { } +=item realtime_card_cybercash + +Attempts to pay this invoice with the CyberCash CashRegister realtime gateway. + +=cut + +sub realtime_card_cybercash { + my $self = shift; + my $cust_main = $self->cust_main; + my $amount = $self->owed; + + return "CyberCash CashRegister real-time card processing not enabled!" + unless $cybercash eq 'cybercash3.2'; + + my $address = $cust_main->address1; + $address .= ", ". $cust_main->address2 if $cust_main->address2; + + #fix exp. date + #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/; + $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/; + my $exp = "$2/$1"; + + # + + my $paybatch = $self->invnum. + '-' . time2str("%y%m%d%H%M%S", time); + + my $payname = $cust_main->payname || + $cust_main->getfield('first').' '.$cust_main->getfield('last'); + + my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country; + + my @full_xaction = ( $xaction, + 'Order-ID' => $paybatch, + 'Amount' => "usd $amount", + 'Card-Number' => $cust_main->getfield('payinfo'), + 'Card-Name' => $payname, + 'Card-Address' => $address, + 'Card-City' => $cust_main->getfield('city'), + 'Card-State' => $cust_main->getfield('state'), + 'Card-Zip' => $cust_main->getfield('zip'), + 'Card-Country' => $country, + 'Card-Exp' => $exp, + ); + + my %result; + %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction); + + if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3 + my $cust_pay = new FS::cust_pay ( { + 'invnum' => $self->invnum, + 'paid' => $amount, + '_date' => '', + 'payby' => 'CARD', + 'payinfo' => $cust_main->payinfo, + 'paybatch' => "$cybercash:$paybatch", + } ); + my $error = $cust_pay->insert; + if ( $error ) { + # gah, even with transactions. + my $e = 'WARNING: Card debited but database not updated - '. + 'error applying payment, invnum #' . $self->invnum. + " (CyberCash Order-ID $paybatch): $error"; + warn $e; + return $e; + } else { + return ''; + } +# } elsif ( $result{'Mstatus'} ne 'failure-bad-money' +# || $options{'report_badcard'} +# ) { + } else { + return 'Cybercash error, invnum #' . + $self->invnum. ':'. $result{'MErrMsg'}; + } + +} + =item batch_card Adds a payment for this invoice to the pending credit card batch (see @@ -839,7 +1037,6 @@ sub batch_card { 'state' => $cust_main->getfield('state'), 'zip' => $cust_main->getfield('zip'), 'country' => $cust_main->getfield('country'), - 'trancode' => 77, 'cardnum' => $cust_main->getfield('payinfo'), 'exp' => $cust_main->getfield('paydate'), 'payname' => $cust_main->getfield('payname'), @@ -851,7 +1048,7 @@ sub batch_card { ''; } -=item print_text [TIME]; +=item print_text [ TIME [ , TEMPLATE ] ] Returns an text invoice, as a list of lines. @@ -896,38 +1093,50 @@ sub print_text { } #new charges - foreach ( ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first - ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes + foreach my $cust_bill_pkg ( + ( grep { $_->pkgnum } $self->cust_bill_pkg ), #packages first + ( grep { ! $_->pkgnum } $self->cust_bill_pkg ), #then taxes ) { - if ( $_->pkgnum ) { + if ( $cust_bill_pkg->pkgnum ) { - my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } ); - my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart}); - my($pkg)=$part_pkg->pkg; + 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 ( $_->setup != 0 ) { - push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ]; + if ( $cust_bill_pkg->setup != 0 ) { + my $description = $pkg; + $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->labels; } - if ( $_->recur != 0 ) { + if ( $cust_bill_pkg->recur != 0 ) { push @buf, [ - "$pkg (" . time2str("%x",$_->sdate) . " - " . - time2str("%x",$_->edate) . ")", - $money_char. sprintf("%10.2f",$_->recur) + "$pkg (" . 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->labels; } - } else { #pkgnum tax - my $itemdesc = defined $_->dbdef_table->column('itemdesc') - ? ( $_->itemdesc || 'Tax' ) + } else { #pkgnum tax or one-shot line item + my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc') + ? ( $cust_bill_pkg->itemdesc || 'Tax' ) : 'Tax'; - push @buf,[$itemdesc, $money_char. sprintf("%10.2f",$_->setup) ] - if $_->setup != 0; + if ( $cust_bill_pkg->setup != 0 ) { + push @buf, [ $itemdesc, + $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). ")", + $money_char. sprintf("%10.2f", $cust_bill_pkg->recur) + ]; + } } } @@ -974,8 +1183,10 @@ sub print_text { } #balance due + my $balance_due_msg = $self->balance_due_msg; + push @buf,['','-----------']; - push @buf,['Balance Due', $money_char. + push @buf,[$balance_due_msg, $money_char. sprintf("%10.2f", $balance_due ) ]; #create the template @@ -985,9 +1196,9 @@ sub print_text { or die "cannot load config file $templatefile"; $invoice_lines = 0; my $wasfunc = 0; - foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy - /invoice_lines\((\d+)\)/; - $invoice_lines += $1; + foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy + /invoice_lines\((\d*)\)/; + $invoice_lines += $1 || scalar(@buf); $wasfunc=1; } die "no invoice_lines() functions in template?" unless $wasfunc; @@ -1000,11 +1211,12 @@ sub print_text { #setup template variables package FS::cust_bill::_template; #! - use vars qw( $invnum $date $page $total_pages @address $overdue @buf ); + use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent ); $invnum = $self->invnum; $date = $self->_date; $page = 1; + $agent = $self->cust_main->agent->agent; if ( $FS::cust_bill::invoice_lines ) { $total_pages = @@ -1045,16 +1257,14 @@ sub print_text { # ); #and subroutine for the template - sub FS::cust_bill::_template::invoice_lines { - my $lines = shift or return @buf; + my $lines = shift || scalar(@buf); map { scalar(@buf) ? shift @buf : [ '', '' ]; } ( 1 .. $lines ); } - #and fill it in $FS::cust_bill::_template::page = 1; my $lines; @@ -1070,11 +1280,506 @@ sub print_text { } -=back +=item print_latex [ TIME [ , TEMPLATE ] ] + +Internal method - returns a filename of a filled-in LaTeX template for this +invoice (Note: add ".tex" to get the actual filename). + +See print_ps and print_pdf for methods that return PostScript and PDF output. + +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 + +#still some false laziness w/print_text +sub print_latex { + + my( $self, $today, $template ) = @_; + $today ||= time; + +# 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'; + + 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 + my $templatefile = 'invoice_latex'; + $templatefile .= "_$template" if $template; + my @invoice_template = $conf->config($templatefile) + or die "cannot load config file $templatefile"; + + my %invoice_data = ( + 'invnum' => $self->invnum, + 'date' => time2str('%b %o, %Y', $self->_date), + 'agent' => _latex_escape($cust_main->agent->agent), + 'payname' => _latex_escape($cust_main->payname), + 'company' => _latex_escape($cust_main->company), + 'address1' => _latex_escape($cust_main->address1), + 'address2' => _latex_escape($cust_main->address2), + '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') ), + 'quantity' => 1, + 'terms' => $conf->config('invoice_default_terms') || 'Payable upon receipt', + #'notes' => join("\n", $conf->config('invoice_latexnotes') ), + ); + + 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('invoice_latexnotes') + ); + + $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 ) + ? _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'}} ); + } + $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'} ); + push @total_fill, + map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } + @total_item; + } + + 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; + + #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'} = + '\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; + } + + } + + sub nounder { + my $var = $1; + $var =~ s/_/\-/g; + $var; + } + + my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/ + my $unique = int(rand(2**31)); #UGH... use File::Temp or something + + chdir($dir); + my $file = $self->invnum. ".$unique"; + + open(TEX,">$file.tex") or die "can't open $file.tex: $!\n"; + print TEX join("\n", @filled_in ), "\n"; + close TEX; + + return $file; + +} + +=item print_ps [ TIME [ , TEMPLATE ] ] + +Returns an postscript 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_ps { + my $self = shift; + + my $file = $self->print_latex(@_); + + #error checking!! + system('pslatex', "$file.tex"); + system('pslatex', "$file.tex"); + system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" ); + + open(POSTSCRIPT, "<$file.ps") + or die "can't open $file.ps (probable error in LaTeX template): $!\n"; + + unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex"); + + my $ps = ''; + while () { + $ps .= $_; + } + + close POSTSCRIPT; + + return $ps; -=head1 VERSION +} + +=item print_pdf [ TIME [ , TEMPLATE ] ] + +Returns an PDF 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_pdf { + my $self = shift; + + my $file = $self->print_latex(@_); + + #system('pdflatex', "$file.tex"); + #system('pdflatex', "$file.tex"); + #! LaTeX Error: Unknown graphics extension: .eps. + + #error checking!! + system('pslatex', "$file.tex"); + system('pslatex', "$file.tex"); + + #system('dvipdf', "$file.dvi", "$file.pdf" ); + system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -"); + + open(PDF, "<$file.pdf") + or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n"; + + unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex"); + + my $pdf = ''; + while () { + $pdf .= $_; + } + + close PDF; + + return $pdf; + +} + +# quick subroutine for print_latex +# +# There are ten characters that LaTeX treats as special characters, which +# means that they do not simply typeset themselves: +# # $ % & ~ _ ^ \ { } +# +# TeX ignores blanks following an escaped character; if you want a blank (as +# in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). + +sub _latex_escape { + my $value = shift; + $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge; + $value; +} + +#utility methods for print_* + +sub balance_due_msg { + my $self = shift; + my $msg = 'Balance Due'; + return $msg unless $conf->exists('invoice_default_terms'); + if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) { + $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) ); + } elsif ( $conf->config('invoice_default_terms') ) { + $msg .= ' - '. $conf->config('invoice_default_terms'); + } + $msg; +} + +sub _items { + my $self = shift; + my @display = scalar(@_) + ? @_ + : qw( _items_previous _items_pkg ); + #: qw( _items_pkg ); + #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments ); + my @b = (); + foreach my $display ( @display ) { + push @b, $self->$display(@_); + } + @b; +} -$Id: cust_bill.pm,v 1.50 2002-10-15 09:54:24 ivan Exp $ +sub _items_previous { + my $self = shift; + my $cust_main = $self->cust_main; + my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance + my @b = (); + foreach ( @pr_cust_bill ) { + push @b, { + 'description' => 'Previous Balance, Invoice #'. $_->invnum. + ' ('. time2str('%x',$_->_date). ')', + #'pkgpart' => 'N/A', + 'pkgnum' => 'N/A', + 'amount' => sprintf("%10.2f", $_->owed), + }; + } + @b; + + #{ + # 'description' => 'Previous Balance', + # #'pkgpart' => 'N/A', + # 'pkgnum' => 'N/A', + # 'amount' => sprintf("%10.2f", $pr_total ), + # 'ext_description' => [ map { + # "Invoice ". $_->invnum. + # " (". time2str("%x",$_->_date). ") ". + # sprintf("%10.2f", $_->owed) + # } @pr_cust_bill ], + + #}; +} + +sub _items_pkg { + my $self = shift; + my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg; + $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); +} + +sub _items_tax { + my $self = shift; + my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg; + $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_); +} + +sub _items_cust_bill_pkg { + my $self = shift; + my $cust_bill_pkg = shift; + + my @b = (); + foreach my $cust_bill_pkg ( @$cust_bill_pkg ) { + + if ( $cust_bill_pkg->pkgnum ) { + + 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; + + 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; + 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, + }; + } + + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + '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, + ], + }; + } + + } 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), + }; + } + if ( $cust_bill_pkg->recur != 0 ) { + push @b, { + 'description' => "$itemdesc (". + time2str("%x", $cust_bill_pkg->sdate). ' - '. + time2str("%x", $cust_bill_pkg->edate). ')', + 'amount' => sprintf("%10.2f", $cust_bill_pkg->recur), + }; + } + + } + + } + + @b; + +} + +sub _items_credits { + my $self = shift; + + my @b; + #credits + foreach ( $self->cust_credited ) { + + #something more elaborate if $_->amount ne $_->cust_credit->credited ? + + my $reason = $_->cust_credit->reason; + #my $reason = substr($_->cust_credit->reason,0,32); + #$reason .= '...' if length($reason) < length($_->cust_credit->reason); + $reason = " ($reason) " if $reason; + push @b, { + #'description' => 'Credit ref\#'. $_->crednum. + # " (". time2str("%x",$_->cust_credit->_date) .")". + # $reason, + 'description' => 'Credit applied'. + time2str("%x",$_->cust_credit->_date). $reason, + 'amount' => sprintf("%10.2f",$_->amount), + }; + } + #foreach ( @cr_cust_credit ) { + # push @buf,[ + # "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")", + # $money_char. sprintf("%10.2f",$_->credited) + # ]; + #} + + @b; + +} + +sub _items_payments { + my $self = shift; + + my @b; + #get & print payments + foreach ( $self->cust_bill_pay ) { + + #something more elaborate if $_->amount ne ->cust_pay->paid ? + + push @b, { + 'description' => "Payment received ". + time2str("%x",$_->cust_pay->_date ), + 'amount' => sprintf("%10.2f", $_->amount ) + }; + } + + @b; + +} + +=back =head1 BUGS