X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fcust_bill.pm;h=367e1bc5ec5d00ca855117347a7da6a2ca8b1006;hp=48315199ab7a64f5fcf36f3d32a210e3b179181f;hb=e975ed0585280f4cbb90b02f57114dedc43f58be;hpb=a7ac7ab3af0dd4f9773aec7c59515fefc8eaedec diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm index 48315199a..367e1bc5e 100644 --- a/FS/FS/cust_bill.pm +++ b/FS/FS/cust_bill.pm @@ -2,18 +2,12 @@ package FS::cust_bill; use strict; use vars qw( @ISA $conf $money_char ); -use vars qw( $lpr $invoice_from $smtpmachine ); -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( $quiet ); use Date::Format; -use Mail::Internet 1.44; -use Mail::Header; use Text::Template; use FS::UID qw( datasrc ); use FS::Record qw( qsearch qsearchs ); +use FS::Misc qw( send_email ); use FS::cust_main; use FS::cust_bill_pkg; use FS::cust_credit; @@ -26,41 +20,10 @@ use FS::cust_bill_event; @ISA = qw( FS::Record ); #ask FS::UID to run this stuff for us later -$FS::UID::callback{'FS::cust_bill'} = sub { - +FS::UID->install_callback( sub { $conf = new FS::Conf; - $money_char = $conf->config('money_char') || '$'; - - $lpr = $conf->config('lpr'); - $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 - ) = $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"; - } - -}; +} ); =head1 NAME @@ -198,7 +161,7 @@ sub check { $self->printed(0) if $self->printed eq ''; - ''; #no error + $self->SUPER::check; } =item previous @@ -353,54 +316,53 @@ sub owed { $balance; } -=item send +=item send [ TEMPLATENAME [ , AGENTNUM ] ] Sends this invoice to the destinations configured for this customer: send 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. + =cut sub send { - my($self,$template) = @_; + my $self = shift; + my $template = scalar(@_) ? shift : ''; + return '' if scalar(@_) && $_[0] && $self->cust_main->agentnum ne shift; + 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 #better to notify this person than silence - @invoicing_list = ($invoice_from) unless @invoicing_list; - - #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card - #$ENV{SMTPHOSTS} = $smtpmachine; - $ENV{MAILADDRESS} = $invoice_from; - my $header = new Mail::Header ( [ - "From: $invoice_from", - "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ), - "Sender: $invoice_from", - "Reply-To: $invoice_from", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: Invoice", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ @print_text ], #( date) + @invoicing_list = ($conf->config('invoice_from')) unless @invoicing_list; + + my $error = send_email( + 'from' => $conf->config('invoice_from'), + 'to' => [ grep { $_ ne 'POST' } @invoicing_list ], + 'subject' => 'Invoice', + 'body' => \@print_text, ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or return "(customer # ". $self->custnum. ") can't send invoice email". - " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ). - " via server $smtpmachine with SMTP: $!"; + die "can't email invoice: $error\n" if $error; } + if ( $conf->config('invoice_latex') ) { + @print_text = $self->print_ps('', $template); + } + if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal + my $lpr = $conf->config('lpr'); open(LPR, "|$lpr") - or return "Can't open pipe to $lpr: $!"; + or die "Can't open pipe to $lpr: $!\n"; print LPR @print_text; close LPR - or return $! ? "Error closing $lpr: $!" - : "Exit status $? from $lpr"; + or die $! ? "Error closing $lpr: $!\n" + : "Exit status $? from $lpr\n"; } ''; @@ -605,15 +567,7 @@ for supported processors. sub realtime_card { my $self = shift; - $self->realtime_bop( - 'CC', - $bop_processor, - $bop_login, - $bop_password, - $bop_action, - \@bop_options, - @_ - ); + $self->realtime_bop( 'CC', @_ ); } =item realtime_ach @@ -627,15 +581,7 @@ for supported processors. sub realtime_ach { my $self = shift; - $self->realtime_bop( - 'ECHECK', - $ach_processor, - $ach_login, - $ach_password, - $ach_action, - \@ach_options, - @_ - ); + $self->realtime_bop( 'ECHECK', @_ ); } =item realtime_lec @@ -649,48 +595,17 @@ for supported processors. sub realtime_lec { my $self = shift; - $self->realtime_bop( - 'LEC', - $bop_processor, - $bop_login, - $bop_password, - $bop_action, - \@bop_options, - @_ - ); + $self->realtime_bop( 'LEC', @_ ); } sub realtime_bop { - my( $self, $method, $processor, $login, $password, $action, $options ) = @_; - my $cust_main = $self->cust_main; - my $amount = $self->owed; - - my $address = $cust_main->address1; - $address .= ", ". $cust_main->address2 if $cust_main->address2; - - my($payname, $payfirst, $paylast); - if ( $cust_main->payname && $method ne 'ECHECK' ) { - $payname = $cust_main->payname; - $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/ - or do { - #$dbh->rollback if $oldAutoCommit; - return "Illegal payname $payname"; - }; - ($payfirst, $paylast) = ($1, $2); - } else { - $payfirst = $cust_main->getfield('first'); - $paylast = $cust_main->getfield('last'); - $payname = "$payfirst $paylast"; - } + my( $self, $method ) = @_; - my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list; - if ( $conf->exists('emailinvoiceauto') - || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) { - push @invoicing_list, $cust_main->all_emails; - } - my $email = $invoicing_list[0]; - - my( $action1, $action2 ) = split(/\s*\,\s*/, $action ); + 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 $description = 'Internet Services'; if ( $conf->exists('business-onlinepayment-description') ) { @@ -705,163 +620,12 @@ sub realtime_bop { grep { $_->pkgnum } $self->cust_bill_pkg ); $description = eval qq("$dtempl"); - } - 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 '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; - } elsif ( $method eq 'LEC' ) { - $content{phone} = $cust_main->payinfo; - } - - my $transaction = - new Business::OnlinePayment( $processor, @$options ); - $transaction->content( - 'type' => $method, - 'login' => $login, - 'password' => $password, - 'action' => $action1, - 'description' => $description, - 'amount' => $amount, - 'invoice_number' => $self->invnum, - 'customer_id' => $self->custnum, - 'last_name' => $paylast, - 'first_name' => $payfirst, - 'name' => $payname, - 'address' => $address, - 'city' => $cust_main->city, - 'state' => $cust_main->state, - 'zip' => $cust_main->zip, - 'country' => $cust_main->country, - 'referer' => 'http://cleanwhisker.420.am/', - 'email' => $email, - 'phone' => $cust_main->daytime || $cust_main->night, - %content, #after + $cust_main->realtime_bop($method, $amount, + 'description' => $description, + 'invnum' => $self->invnum, ); - $transaction->submit(); - - if ( $transaction->is_success() && $action2 ) { - my $auth = $transaction->authorization; - my $ordernum = $transaction->can('order_number') - ? $transaction->order_number - : ''; - - #warn "********* $auth ***********\n"; - #warn "********* $ordernum ***********\n"; - my $capture = - new Business::OnlinePayment( $processor, @$options ); - - my %capture = ( - %content, - type => $method, - action => $action2, - login => $login, - password => $password, - order_number => $ordernum, - amount => $amount, - authorization => $auth, - description => $description, - ); - - foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code - transaction_sequence_num local_transaction_date - local_transaction_time AVS_result_code )) { - $capture{$field} = $transaction->$field() if $transaction->can($field); - } - - $capture->content( %capture ); - - $capture->submit(); - - unless ( $capture->is_success ) { - my $e = "Authorization sucessful but capture failed, invnum #". - $self->invnum. ': '. $capture->result_code. - ": ". $capture->error_message; - warn $e; - return $e; - } - - } - - if ( $transaction->is_success() ) { - - my %method2payby = ( - 'CC' => 'CARD', - 'ECHECK' => 'CHEK', - 'LEC' => 'LECB', - ); - - my $cust_pay = new FS::cust_pay ( { - 'invnum' => $self->invnum, - 'paid' => $amount, - '_date' => '', - 'payby' => method2payby{$method}, - 'payinfo' => $cust_main->payinfo, - 'paybatch' => "$processor:". $transaction->authorization, - } ); - my $error = $cust_pay->insert; - if ( $error ) { - # gah, even with transactions. - my $e = 'WARNING: Card/ACH debited but database not updated - '. - 'error applying payment, invnum #' . $self->invnum. - " ($processor): $error"; - warn $e; - return $e; - } else { - return ''; - } - #} elsif ( $options{'report_badcard'} ) { - } else { - - my $perror = "$processor error, invnum #". $self->invnum. ': '. - $transaction->result_code. ": ". $transaction->error_message; - - if ( !$quiet && $conf->exists('emaildecline') - && grep { $_ ne 'POST' } $cust_main->invoicing_list - ) { - my @templ = $conf->config('declinetemplate'); - my $template = new Text::Template ( - TYPE => 'ARRAY', - SOURCE => [ map "$_\n", @templ ], - ) or return "($perror) can't create template: $Text::Template::ERROR"; - $template->compile() - or return "($perror) can't compile template: $Text::Template::ERROR"; - - my $templ_hash = { error => $transaction->error_message }; - - #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send - $ENV{MAILADDRESS} = $invoice_from; - my $header = new Mail::Header ( [ - "From: $invoice_from", - "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ), - "Sender: $invoice_from", - "Reply-To: $invoice_from", - "Date: ". time2str("%a, %d %b %Y %X %z", time), - "Subject: Your payment could not be processed", - ] ); - my $message = new Mail::Internet ( - 'Header' => $header, - 'Body' => [ $template->fill_in(HASH => $templ_hash) ], - ); - $!=0; - $message->smtpsend( Host => $smtpmachine ) - or $message->smtpsend( Host => $smtpmachine, Debug => 1 ) - or return "($perror) (customer # ". $self->custnum. - ") can't send card decline email to ". - join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ). - " via server $smtpmachine with SMTP: $!"; - } - - return $perror; - } } @@ -887,7 +651,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'), @@ -899,7 +662,32 @@ sub batch_card { ''; } -=item print_text [TIME]; +sub _agent_template { + my $self = shift; + + my $cust_bill_event = qsearchs( 'part_bill_event', + { + 'payby' => $self->cust_main->payby, + 'plan' => 'send_agent', + 'eventcode' => { 'op' => 'LIKE', + 'value' => '_%, '. $self->cust_main->agentnum. ');' }, + }, + '', + 'ORDER BY seconds LIMIT 1' + ); + + return '' unless $cust_bill_event; + + if ( $cust_bill_event->eventcode =~ /\(\s*'(.*)'\s*,\s*(\d+)\s*\)\;$/ ) { + return $1; + } else { + warn "can't parse eventcode for agent-specific invoice template"; + return ''; + } + +} + +=item print_text [ TIME [ , TEMPLATE ] ] Returns an text invoice, as a list of lines. @@ -944,38 +732,52 @@ 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' ) + 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'; - 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) + ]; + } } } @@ -1022,15 +824,18 @@ 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 + $template ||= $self->_agent_template; my $templatefile = 'invoice_template'; - $templatefile .= "_$template" if $template; + $templatefile .= "_$template" if length($template); my @invoice_template = $conf->config($templatefile) - or die "cannot load config file $templatefile"; + or die "cannot load config file $templatefile"; $invoice_lines = 0; my $wasfunc = 0; foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy @@ -1048,11 +853,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 = @@ -1116,11 +922,508 @@ 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). -=head1 VERSION +See print_ps and print_pdf for methods that return PostScript and PDF output. -$Id: cust_bill.pm,v 1.58 2002-12-23 14:22:48 steve Exp $ +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 + $template ||= $self->_agent_template; + my $templatefile = 'invoice_latex'; + my $suffix = length($template) ? "_$template" : ''; + $templatefile .= $suffix; + 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_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 ) + ? _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; + +} + +=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; +} + +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 @@ -1129,9 +1432,6 @@ The delete method. print_text formatting (and some logic :/) is in source, but needs to be slurped in from a file. Also number of lines ($=). -missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style -or something similar so the look can be completely customized?) - =head1 SEE ALSO L, L, L, L,