postscript invoice redux
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $conf $money_char );
5 use vars qw( $lpr $invoice_from $smtpmachine );
6 use vars qw( $cybercash );
7 use vars qw( $xaction $E_NoErr );
8 use vars qw( $bop_processor $bop_login $bop_password $bop_action @bop_options );
9 use vars qw( $ach_processor $ach_login $ach_password $ach_action @ach_options );
10 use vars qw( $invoice_lines @buf ); #yuck
11 use vars qw( $realtime_bop_decline_quiet );
12 use Date::Format;
13 use Mail::Internet 1.44;
14 use Mail::Header;
15 use Text::Template;
16 use FS::UID qw( datasrc );
17 use FS::Record qw( qsearch qsearchs );
18 use FS::cust_main;
19 use FS::cust_bill_pkg;
20 use FS::cust_credit;
21 use FS::cust_pay;
22 use FS::cust_pkg;
23 use FS::cust_credit_bill;
24 use FS::cust_pay_batch;
25 use FS::cust_bill_event;
26
27 @ISA = qw( FS::Record );
28
29 $realtime_bop_decline_quiet = 0;
30
31 #ask FS::UID to run this stuff for us later
32 $FS::UID::callback{'FS::cust_bill'} = sub { 
33
34   $conf = new FS::Conf;
35
36   $money_char = $conf->config('money_char') || '$';  
37
38   $lpr = $conf->config('lpr');
39   $invoice_from = $conf->config('invoice_from');
40   $smtpmachine = $conf->config('smtpmachine');
41
42   ( $bop_processor,$bop_login, $bop_password, $bop_action ) = ( '', '', '', '');
43   @bop_options = ();
44   ( $ach_processor,$ach_login, $ach_password, $ach_action ) = ( '', '', '', '');
45   @ach_options = ();
46
47   if ( $conf->exists('cybercash3.2') ) {
48     require CCMckLib3_2;
49       #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
50     require CCMckDirectLib3_2;
51       #qw(SendCC2_1Server);
52     require CCMckErrno3_2;
53       #qw(MCKGetErrorMessage $E_NoErr);
54     import CCMckErrno3_2 qw($E_NoErr);
55
56     my $merchant_conf;
57     ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
58     my $status = &CCMckLib3_2::InitConfig($merchant_conf);
59     if ( $status != $E_NoErr ) {
60       warn "CCMckLib3_2::InitConfig error:\n";
61       foreach my $key (keys %CCMckLib3_2::Config) {
62         warn "  $key => $CCMckLib3_2::Config{$key}\n"
63       }
64       my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
65       die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
66     }
67     $cybercash='cybercash3.2';
68   } elsif ( $conf->exists('business-onlinepayment') ) {
69     ( $bop_processor,
70       $bop_login,
71       $bop_password,
72       $bop_action,
73       @bop_options
74     ) = $conf->config('business-onlinepayment');
75     $bop_action ||= 'normal authorization';
76     ( $ach_processor, $ach_login, $ach_password, $ach_action, @ach_options ) =
77       ( $bop_processor, $bop_login, $bop_password, $bop_action, @bop_options );
78     eval "use Business::OnlinePayment";  
79   }
80
81   if ( $conf->exists('business-onlinepayment-ach') ) {
82     ( $ach_processor,
83       $ach_login,
84       $ach_password,
85       $ach_action,
86       @ach_options
87     ) = $conf->config('business-onlinepayment-ach');
88     $ach_action ||= 'normal authorization';
89     eval "use Business::OnlinePayment";  
90   }
91
92 };
93
94 =head1 NAME
95
96 FS::cust_bill - Object methods for cust_bill records
97
98 =head1 SYNOPSIS
99
100   use FS::cust_bill;
101
102   $record = new FS::cust_bill \%hash;
103   $record = new FS::cust_bill { 'column' => 'value' };
104
105   $error = $record->insert;
106
107   $error = $new_record->replace($old_record);
108
109   $error = $record->delete;
110
111   $error = $record->check;
112
113   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
114
115   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
116
117   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
118
119   @cust_pay_objects = $cust_bill->cust_pay;
120
121   $tax_amount = $record->tax;
122
123   @lines = $cust_bill->print_text;
124   @lines = $cust_bill->print_text $time;
125
126 =head1 DESCRIPTION
127
128 An FS::cust_bill object represents an invoice; a declaration that a customer
129 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
130 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
131 following fields are currently supported:
132
133 =over 4
134
135 =item invnum - primary key (assigned automatically for new invoices)
136
137 =item custnum - customer (see L<FS::cust_main>)
138
139 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
140 L<Time::Local> and L<Date::Parse> for conversion functions.
141
142 =item charged - amount of this invoice
143
144 =item printed - deprecated
145
146 =item closed - books closed flag, empty or `Y'
147
148 =back
149
150 =head1 METHODS
151
152 =over 4
153
154 =item new HASHREF
155
156 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
157 Invoices are normally created by calling the bill method of a customer object
158 (see L<FS::cust_main>).
159
160 =cut
161
162 sub table { 'cust_bill'; }
163
164 =item insert
165
166 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
167 returns the error, otherwise returns false.
168
169 =item delete
170
171 Currently unimplemented.  I don't remove invoices because there would then be
172 no record you ever posted this invoice (which is bad, no?)
173
174 =cut
175
176 sub delete {
177   my $self = shift;
178   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
179   $self->SUPER::delete(@_);
180 }
181
182 =item replace OLD_RECORD
183
184 Replaces the OLD_RECORD with this one in the database.  If there is an error,
185 returns the error, otherwise returns false.
186
187 Only printed may be changed.  printed is normally updated by calling the
188 collect method of a customer object (see L<FS::cust_main>).
189
190 =cut
191
192 sub replace {
193   my( $new, $old ) = ( shift, shift );
194   return "Can't change custnum!" unless $old->custnum == $new->custnum;
195   #return "Can't change _date!" unless $old->_date eq $new->_date;
196   return "Can't change _date!" unless $old->_date == $new->_date;
197   return "Can't change charged!" unless $old->charged == $new->charged;
198
199   $new->SUPER::replace($old);
200 }
201
202 =item check
203
204 Checks all fields to make sure this is a valid invoice.  If there is an error,
205 returns the error, otherwise returns false.  Called by the insert and replace
206 methods.
207
208 =cut
209
210 sub check {
211   my $self = shift;
212
213   my $error =
214     $self->ut_numbern('invnum')
215     || $self->ut_number('custnum')
216     || $self->ut_numbern('_date')
217     || $self->ut_money('charged')
218     || $self->ut_numbern('printed')
219     || $self->ut_enum('closed', [ '', 'Y' ])
220   ;
221   return $error if $error;
222
223   return "Unknown customer"
224     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
225
226   $self->_date(time) unless $self->_date;
227
228   $self->printed(0) if $self->printed eq '';
229
230   ''; #no error
231 }
232
233 =item previous
234
235 Returns a list consisting of the total previous balance for this customer, 
236 followed by the previous outstanding invoices (as FS::cust_bill objects also).
237
238 =cut
239
240 sub previous {
241   my $self = shift;
242   my $total = 0;
243   my @cust_bill = sort { $a->_date <=> $b->_date }
244     grep { $_->owed != 0 && $_->_date < $self->_date }
245       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
246   ;
247   foreach ( @cust_bill ) { $total += $_->owed; }
248   $total, @cust_bill;
249 }
250
251 =item cust_bill_pkg
252
253 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
254
255 =cut
256
257 sub cust_bill_pkg {
258   my $self = shift;
259   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
260 }
261
262 =item cust_bill_event
263
264 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
265 invoice.
266
267 =cut
268
269 sub cust_bill_event {
270   my $self = shift;
271   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
272 }
273
274
275 =item cust_main
276
277 Returns the customer (see L<FS::cust_main>) for this invoice.
278
279 =cut
280
281 sub cust_main {
282   my $self = shift;
283   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
284 }
285
286 =item cust_credit
287
288 Depreciated.  See the cust_credited method.
289
290  #Returns a list consisting of the total previous credited (see
291  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
292  #outstanding credits (FS::cust_credit objects).
293
294 =cut
295
296 sub cust_credit {
297   use Carp;
298   croak "FS::cust_bill->cust_credit depreciated; see ".
299         "FS::cust_bill->cust_credit_bill";
300   #my $self = shift;
301   #my $total = 0;
302   #my @cust_credit = sort { $a->_date <=> $b->_date }
303   #  grep { $_->credited != 0 && $_->_date < $self->_date }
304   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
305   #;
306   #foreach (@cust_credit) { $total += $_->credited; }
307   #$total, @cust_credit;
308 }
309
310 =item cust_pay
311
312 Depreciated.  See the cust_bill_pay method.
313
314 #Returns all payments (see L<FS::cust_pay>) for this invoice.
315
316 =cut
317
318 sub cust_pay {
319   use Carp;
320   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
321   #my $self = shift;
322   #sort { $a->_date <=> $b->_date }
323   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
324   #;
325 }
326
327 =item cust_bill_pay
328
329 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
330
331 =cut
332
333 sub cust_bill_pay {
334   my $self = shift;
335   sort { $a->_date <=> $b->_date }
336     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
337 }
338
339 =item cust_credited
340
341 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
342
343 =cut
344
345 sub cust_credited {
346   my $self = shift;
347   sort { $a->_date <=> $b->_date }
348     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
349   ;
350 }
351
352 =item tax
353
354 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
355
356 =cut
357
358 sub tax {
359   my $self = shift;
360   my $total = 0;
361   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
362                                              'pkgnum' => 0 } );
363   foreach (@taxlines) { $total += $_->setup; }
364   $total;
365 }
366
367 =item owed
368
369 Returns the amount owed (still outstanding) on this invoice, which is charged
370 minus all payment applications (see L<FS::cust_bill_pay>) and credit
371 applications (see L<FS::cust_credit_bill>).
372
373 =cut
374
375 sub owed {
376   my $self = shift;
377   my $balance = $self->charged;
378   $balance -= $_->amount foreach ( $self->cust_bill_pay );
379   $balance -= $_->amount foreach ( $self->cust_credited );
380   $balance = sprintf( "%.2f", $balance);
381   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
382   $balance;
383 }
384
385 =item send
386
387 Sends this invoice to the destinations configured for this customer: send
388 emails or print.  See L<FS::cust_main_invoice>.
389
390 =cut
391
392 sub send {
393   my($self,$template) = @_;
394   my @print_text = $self->print_text('', $template);
395   my @invoicing_list = $self->cust_main->invoicing_list;
396
397   if ( grep { $_ ne 'POST' } @invoicing_list or !@invoicing_list ) { #email
398
399     #better to notify this person than silence
400     @invoicing_list = ($invoice_from) unless @invoicing_list;
401
402     #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
403     #$ENV{SMTPHOSTS} = $smtpmachine;
404     $ENV{MAILADDRESS} = $invoice_from;
405     my $header = new Mail::Header ( [
406       "From: $invoice_from",
407       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
408       "Sender: $invoice_from",
409       "Reply-To: $invoice_from",
410       "Date: ". time2str("%a, %d %b %Y %X %z", time),
411       "Subject: Invoice",
412     ] );
413     my $message = new Mail::Internet (
414       'Header' => $header,
415       'Body' => [ @print_text ], #( date)
416     );
417     $!=0;
418     $message->smtpsend( Host => $smtpmachine )
419       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
420         or return "(customer # ". $self->custnum. ") can't send invoice email".
421                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
422                   " via server $smtpmachine with SMTP: $!";
423
424   }
425
426   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
427     open(LPR, "|$lpr")
428       or return "Can't open pipe to $lpr: $!";
429     print LPR @print_text;
430     close LPR
431       or return $! ? "Error closing $lpr: $!"
432                    : "Exit status $? from $lpr";
433   }
434
435   '';
436
437 }
438
439 =item send_csv OPTIONS
440
441 Sends invoice as a CSV data-file to a remote host with the specified protocol.
442
443 Options are:
444
445 protocol - currently only "ftp"
446 server
447 username
448 password
449 dir
450
451 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
452 and YYMMDDHHMMSS is a timestamp.
453
454 The fields of the CSV file is as follows:
455
456 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
457
458 =over 4
459
460 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
461
462 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
463 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
464 fields are filled in.
465
466 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
467 first two fields (B<record_type> and B<invnum>) and the last five fields
468 (B<pkg> through B<edate>) are filled in.
469
470 =item invnum - invoice number
471
472 =item custnum - customer number
473
474 =item _date - invoice date
475
476 =item charged - total invoice amount
477
478 =item first - customer first name
479
480 =item last - customer first name
481
482 =item company - company name
483
484 =item address1 - address line 1
485
486 =item address2 - address line 1
487
488 =item city
489
490 =item state
491
492 =item zip
493
494 =item country
495
496 =item pkg - line item description
497
498 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
499
500 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
501
502 =item sdate - start date for recurring fee
503
504 =item edate - end date for recurring fee
505
506 =back
507
508 =cut
509
510 sub send_csv {
511   my($self, %opt) = @_;
512
513   #part one: create file
514
515   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
516   mkdir $spooldir, 0700 unless -d $spooldir;
517
518   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
519
520   open(CSV, ">$file") or die "can't open $file: $!";
521
522   eval "use Text::CSV_XS";
523   die $@ if $@;
524
525   my $csv = Text::CSV_XS->new({'always_quote'=>1});
526
527   my $cust_main = $self->cust_main;
528
529   $csv->combine(
530     'cust_bill',
531     $self->invnum,
532     $self->custnum,
533     time2str("%x", $self->_date),
534     sprintf("%.2f", $self->charged),
535     ( map { $cust_main->getfield($_) }
536         qw( first last company address1 address2 city state zip country ) ),
537     map { '' } (1..5),
538   ) or die "can't create csv";
539   print CSV $csv->string. "\n";
540
541   #new charges (false laziness w/print_text)
542   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
543
544     my($pkg, $setup, $recur, $sdate, $edate);
545     if ( $cust_bill_pkg->pkgnum ) {
546     
547       ($pkg, $setup, $recur, $sdate, $edate) = (
548         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
549         ( $cust_bill_pkg->setup != 0
550           ? sprintf("%.2f", $cust_bill_pkg->setup )
551           : '' ),
552         ( $cust_bill_pkg->recur != 0
553           ? sprintf("%.2f", $cust_bill_pkg->recur )
554           : '' ),
555         time2str("%x", $cust_bill_pkg->sdate),
556         time2str("%x", $cust_bill_pkg->edate),
557       );
558
559     } else { #pkgnum Tax
560       next unless $cust_bill_pkg->setup != 0;
561       ($pkg, $setup, $recur, $sdate, $edate) =
562         ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
563     }
564
565     $csv->combine(
566       'cust_bill_pkg',
567       $self->invnum,
568       ( map { '' } (1..11) ),
569       ($pkg, $setup, $recur, $sdate, $edate)
570     ) or die "can't create csv";
571     print CSV $csv->string. "\n";
572
573   }
574
575   close CSV or die "can't close CSV: $!";
576
577   #part two: upload it
578
579   my $net;
580   if ( $opt{protocol} eq 'ftp' ) {
581     eval "use Net::FTP;";
582     die $@ if $@;
583     $net = Net::FTP->new($opt{server}) or die @$;
584   } else {
585     die "unknown protocol: $opt{protocol}";
586   }
587
588   $net->login( $opt{username}, $opt{password} )
589     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
590
591   $net->binary or die "can't set binary mode";
592
593   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
594
595   $net->put($file) or die "can't put $file: $!";
596
597   $net->quit;
598
599   unlink $file;
600
601 }
602
603 =item comp
604
605 Pays this invoice with a compliemntary payment.  If there is an error,
606 returns the error, otherwise returns false.
607
608 =cut
609
610 sub comp {
611   my $self = shift;
612   my $cust_pay = new FS::cust_pay ( {
613     'invnum'   => $self->invnum,
614     'paid'     => $self->owed,
615     '_date'    => '',
616     'payby'    => 'COMP',
617     'payinfo'  => $self->cust_main->payinfo,
618     'paybatch' => '',
619   } );
620   $cust_pay->insert;
621 }
622
623 =item realtime_card
624
625 Attempts to pay this invoice with a credit card payment via a
626 Business::OnlinePayment realtime gateway.  See
627 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
628 for supported processors.
629
630 =cut
631
632 sub realtime_card {
633   my $self = shift;
634   $self->realtime_bop(
635     'CC',
636     $bop_processor,
637     $bop_login,
638     $bop_password,
639     $bop_action,
640     \@bop_options,
641     @_
642   );
643 }
644
645 =item realtime_ach
646
647 Attempts to pay this invoice with an electronic check (ACH) payment via a
648 Business::OnlinePayment realtime gateway.  See
649 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
650 for supported processors.
651
652 =cut
653
654 sub realtime_ach {
655   my $self = shift;
656   $self->realtime_bop(
657     'ECHECK',
658     $ach_processor,
659     $ach_login,
660     $ach_password,
661     $ach_action,
662     \@ach_options,
663     @_
664   );
665 }
666
667 =item realtime_lec
668
669 Attempts to pay this invoice with phone bill (LEC) payment via a
670 Business::OnlinePayment realtime gateway.  See
671 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
672 for supported processors.
673
674 =cut
675
676 sub realtime_lec {
677   my $self = shift;
678   $self->realtime_bop(
679     'LEC',
680     $bop_processor,
681     $bop_login,
682     $bop_password,
683     $bop_action,
684     \@bop_options,
685     @_
686   );
687 }
688
689 sub realtime_bop {
690   my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
691
692   #trim an extraneous blank line
693   pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
694
695   my $cust_main = $self->cust_main;
696   my $amount = $self->owed;
697
698   my $address = $cust_main->address1;
699   $address .= ", ". $cust_main->address2 if $cust_main->address2;
700
701   my($payname, $payfirst, $paylast);
702   if ( $cust_main->payname && $method ne 'ECHECK' ) {
703     $payname = $cust_main->payname;
704     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
705       or do {
706               #$dbh->rollback if $oldAutoCommit;
707               return "Illegal payname $payname";
708             };
709     ($payfirst, $paylast) = ($1, $2);
710   } else {
711     $payfirst = $cust_main->getfield('first');
712     $paylast = $cust_main->getfield('last');
713     $payname =  "$payfirst $paylast";
714   }
715
716   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
717   if ( $conf->exists('emailinvoiceauto')
718        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
719     push @invoicing_list, $cust_main->all_emails;
720   }
721   my $email = $invoicing_list[0];
722
723   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
724
725   my $description = 'Internet Services';
726   if ( $conf->exists('business-onlinepayment-description') ) {
727     my $dtempl = $conf->config('business-onlinepayment-description');
728
729     my $agent_obj = $cust_main->agent
730       or die "can't retreive agent for $cust_main (agentnum ".
731              $cust_main->agentnum. ")";
732     my $agent = $agent_obj->agent;
733     my $pkgs = join(', ',
734       map { $_->cust_pkg->part_pkg->pkg }
735         grep { $_->pkgnum } $self->cust_bill_pkg
736     );
737     $description = eval qq("$dtempl");
738
739   }
740
741   my %content;
742   if ( $method eq 'CC' ) { 
743
744     $content{card_number} = $cust_main->payinfo;
745     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
746     $content{expiration} = "$2/$1";
747
748     $content{cvv2} = $cust_main->paycvv
749       if defined $cust_main->dbdef_table->column('paycvv')
750          && length($cust_main->paycvv);
751
752     $content{recurring_billing} = 'YES'
753       if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
754                                'payby'   => 'CARD',
755                                'payinfo' => $cust_main->payinfo, } );
756
757   } elsif ( $method eq 'ECHECK' ) {
758     my($account_number,$routing_code) = $cust_main->payinfo;
759     ( $content{account_number}, $content{routing_code} ) =
760       split('@', $cust_main->payinfo);
761     $content{bank_name} = $cust_main->payname;
762     $content{account_type} = 'CHECKING';
763     $content{account_name} = $payname;
764     $content{customer_org} = $self->company ? 'B' : 'I';
765     $content{customer_ssn} = $self->ss;
766   } elsif ( $method eq 'LEC' ) {
767     $content{phone} = $cust_main->payinfo;
768   }
769   
770   my $transaction =
771     new Business::OnlinePayment( $processor, @$options );
772   $transaction->content(
773     'type'           => $method,
774     'login'          => $login,
775     'password'       => $password,
776     'action'         => $action1,
777     'description'    => $description,
778     'amount'         => $amount,
779     'invoice_number' => $self->invnum,
780     'customer_id'    => $self->custnum,
781     'last_name'      => $paylast,
782     'first_name'     => $payfirst,
783     'name'           => $payname,
784     'address'        => $address,
785     'city'           => $cust_main->city,
786     'state'          => $cust_main->state,
787     'zip'            => $cust_main->zip,
788     'country'        => $cust_main->country,
789     'referer'        => 'http://cleanwhisker.420.am/',
790     'email'          => $email,
791     'phone'          => $cust_main->daytime || $cust_main->night,
792     %content, #after
793   );
794   $transaction->submit();
795
796   if ( $transaction->is_success() && $action2 ) {
797     my $auth = $transaction->authorization;
798     my $ordernum = $transaction->can('order_number')
799                    ? $transaction->order_number
800                    : '';
801
802     #warn "********* $auth ***********\n";
803     #warn "********* $ordernum ***********\n";
804     my $capture =
805       new Business::OnlinePayment( $processor, @$options );
806
807     my %capture = (
808       %content,
809       type           => $method,
810       action         => $action2,
811       login          => $login,
812       password       => $password,
813       order_number   => $ordernum,
814       amount         => $amount,
815       authorization  => $auth,
816       description    => $description,
817     );
818
819     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
820                            transaction_sequence_num local_transaction_date    
821                            local_transaction_time AVS_result_code          )) {
822       $capture{$field} = $transaction->$field() if $transaction->can($field);
823     }
824
825     $capture->content( %capture );
826
827     $capture->submit();
828
829     unless ( $capture->is_success ) {
830       my $e = "Authorization sucessful but capture failed, invnum #".
831               $self->invnum. ': '.  $capture->result_code.
832               ": ". $capture->error_message;
833       warn $e;
834       return $e;
835     }
836
837   }
838
839   #remove paycvv after initial transaction
840   #make this disable-able via a config option if anyone insists?  
841   # (though that probably violates cardholder agreements)
842   use Business::CreditCard;
843   if ( defined $cust_main->dbdef_table->column('paycvv')
844        && length($cust_main->paycvv)
845        && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
846
847   ) {
848     my $new = new FS::cust_main { $cust_main->hash };
849     $new->paycvv('');
850     my $error = $new->replace($cust_main);
851     if ( $error ) {
852       warn "error removing cvv: $error\n";
853     }
854   }
855
856   #result handling
857   if ( $transaction->is_success() ) {
858
859     my %method2payby = (
860       'CC'     => 'CARD',
861       'ECHECK' => 'CHEK',
862       'LEC'    => 'LECB',
863     );
864
865     my $cust_pay = new FS::cust_pay ( {
866        'invnum'   => $self->invnum,
867        'paid'     => $amount,
868        '_date'     => '',
869        'payby'    => $method2payby{$method},
870        'payinfo'  => $cust_main->payinfo,
871        'paybatch' => "$processor:". $transaction->authorization,
872     } );
873     my $error = $cust_pay->insert;
874     if ( $error ) {
875       # gah, even with transactions.
876       my $e = 'WARNING: Card/ACH debited but database not updated - '.
877               'error applying payment, invnum #' . $self->invnum.
878               " ($processor): $error";
879       warn $e;
880       return $e;
881     } else {
882       return '';
883     }
884   #} elsif ( $options{'report_badcard'} ) {
885   } else {
886
887     my $perror = "$processor error, invnum #". $self->invnum. ': '.
888                  $transaction->result_code. ": ". $transaction->error_message;
889
890     if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
891          && grep { $_ ne 'POST' } $cust_main->invoicing_list
892          && ! grep { $_ eq $transaction->error_message }
893                    $conf->config('emaildecline-exclude')
894     ) {
895       my @templ = $conf->config('declinetemplate');
896       my $template = new Text::Template (
897         TYPE   => 'ARRAY',
898         SOURCE => [ map "$_\n", @templ ],
899       ) or return "($perror) can't create template: $Text::Template::ERROR";
900       $template->compile()
901         or return "($perror) can't compile template: $Text::Template::ERROR";
902
903       my $templ_hash = { error => $transaction->error_message };
904
905       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
906       $ENV{MAILADDRESS} = $invoice_from;
907       my $header = new Mail::Header ( [
908         "From: $invoice_from",
909         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
910         "Sender: $invoice_from",
911         "Reply-To: $invoice_from",
912         "Date: ". time2str("%a, %d %b %Y %X %z", time),
913         "Subject: Your payment could not be processed",
914       ] );
915       my $message = new Mail::Internet (
916         'Header' => $header,
917         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
918       );
919       $!=0;
920       $message->smtpsend( Host => $smtpmachine )
921         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
922           or return "($perror) (customer # ". $self->custnum.
923             ") can't send card decline email to ".
924             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
925             " via server $smtpmachine with SMTP: $!";
926     }
927   
928     return $perror;
929   }
930
931 }
932
933 =item realtime_card_cybercash
934
935 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
936
937 =cut
938
939 sub realtime_card_cybercash {
940   my $self = shift;
941   my $cust_main = $self->cust_main;
942   my $amount = $self->owed;
943
944   return "CyberCash CashRegister real-time card processing not enabled!"
945     unless $cybercash eq 'cybercash3.2';
946
947   my $address = $cust_main->address1;
948   $address .= ", ". $cust_main->address2 if $cust_main->address2;
949
950   #fix exp. date
951   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
952   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
953   my $exp = "$2/$1";
954
955   #
956
957   my $paybatch = $self->invnum. 
958                   '-' . time2str("%y%m%d%H%M%S", time);
959
960   my $payname = $cust_main->payname ||
961                 $cust_main->getfield('first').' '.$cust_main->getfield('last');
962
963   my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
964
965   my @full_xaction = ( $xaction,
966     'Order-ID'     => $paybatch,
967     'Amount'       => "usd $amount",
968     'Card-Number'  => $cust_main->getfield('payinfo'),
969     'Card-Name'    => $payname,
970     'Card-Address' => $address,
971     'Card-City'    => $cust_main->getfield('city'),
972     'Card-State'   => $cust_main->getfield('state'),
973     'Card-Zip'     => $cust_main->getfield('zip'),
974     'Card-Country' => $country,
975     'Card-Exp'     => $exp,
976   );
977
978   my %result;
979   %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
980   
981   if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
982     my $cust_pay = new FS::cust_pay ( {
983        'invnum'   => $self->invnum,
984        'paid'     => $amount,
985        '_date'     => '',
986        'payby'    => 'CARD',
987        'payinfo'  => $cust_main->payinfo,
988        'paybatch' => "$cybercash:$paybatch",
989     } );
990     my $error = $cust_pay->insert;
991     if ( $error ) {
992       # gah, even with transactions.
993       my $e = 'WARNING: Card debited but database not updated - '.
994               'error applying payment, invnum #' . $self->invnum.
995               " (CyberCash Order-ID $paybatch): $error";
996       warn $e;
997       return $e;
998     } else {
999       return '';
1000     }
1001 #  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1002 #            || $options{'report_badcard'}
1003 #          ) {
1004   } else {
1005      return 'Cybercash error, invnum #' . 
1006        $self->invnum. ':'. $result{'MErrMsg'};
1007   }
1008
1009 }
1010
1011 =item batch_card
1012
1013 Adds a payment for this invoice to the pending credit card batch (see
1014 L<FS::cust_pay_batch>).
1015
1016 =cut
1017
1018 sub batch_card {
1019   my $self = shift;
1020   my $cust_main = $self->cust_main;
1021
1022   my $cust_pay_batch = new FS::cust_pay_batch ( {
1023     'invnum'   => $self->getfield('invnum'),
1024     'custnum'  => $cust_main->getfield('custnum'),
1025     'last'     => $cust_main->getfield('last'),
1026     'first'    => $cust_main->getfield('first'),
1027     'address1' => $cust_main->getfield('address1'),
1028     'address2' => $cust_main->getfield('address2'),
1029     'city'     => $cust_main->getfield('city'),
1030     'state'    => $cust_main->getfield('state'),
1031     'zip'      => $cust_main->getfield('zip'),
1032     'country'  => $cust_main->getfield('country'),
1033     'cardnum'  => $cust_main->getfield('payinfo'),
1034     'exp'      => $cust_main->getfield('paydate'),
1035     'payname'  => $cust_main->getfield('payname'),
1036     'amount'   => $self->owed,
1037   } );
1038   my $error = $cust_pay_batch->insert;
1039   die $error if $error;
1040
1041   '';
1042 }
1043
1044 =item print_text [ TIME [ , TEMPLATE ] ]
1045
1046 Returns an text invoice, as a list of lines.
1047
1048 TIME an optional value used to control the printing of overdue messages.  The
1049 default is now.  It isn't the date of the invoice; that's the `_date' field.
1050 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1051 L<Time::Local> and L<Date::Parse> for conversion functions.
1052
1053 =cut
1054
1055 sub print_text {
1056
1057   my( $self, $today, $template ) = @_;
1058   $today ||= time;
1059 #  my $invnum = $self->invnum;
1060   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1061   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1062     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1063
1064   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1065 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1066   #my $balance_due = $self->owed + $pr_total - $cr_total;
1067   my $balance_due = $self->owed + $pr_total;
1068
1069   #my @collect = ();
1070   #my($description,$amount);
1071   @buf = ();
1072
1073   #previous balance
1074   foreach ( @pr_cust_bill ) {
1075     push @buf, [
1076       "Previous Balance, Invoice #". $_->invnum. 
1077                  " (". time2str("%x",$_->_date). ")",
1078       $money_char. sprintf("%10.2f",$_->owed)
1079     ];
1080   }
1081   if (@pr_cust_bill) {
1082     push @buf,['','-----------'];
1083     push @buf,[ 'Total Previous Balance',
1084                 $money_char. sprintf("%10.2f",$pr_total ) ];
1085     push @buf,['',''];
1086   }
1087
1088   #new charges
1089   foreach my $cust_bill_pkg (
1090     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1091     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1092   ) {
1093
1094     if ( $cust_bill_pkg->pkgnum ) {
1095
1096       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1097       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1098       my $pkg = $part_pkg->pkg;
1099
1100       if ( $cust_bill_pkg->setup != 0 ) {
1101         my $description = $pkg;
1102         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1103         push @buf, [ $description,
1104                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1105         push @buf,
1106           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1107       }
1108
1109       if ( $cust_bill_pkg->recur != 0 ) {
1110         push @buf, [
1111           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1112                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1113           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1114         ];
1115         push @buf,
1116           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1117       }
1118
1119     } else { #pkgnum tax or one-shot line item
1120       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1121                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1122                      : 'Tax';
1123       if ( $cust_bill_pkg->setup != 0 ) {
1124         push @buf, [ $itemdesc,
1125                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1126       }
1127       if ( $cust_bill_pkg->recur != 0 ) {
1128         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1129                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1130                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1131                    ];
1132       }
1133     }
1134   }
1135
1136   push @buf,['','-----------'];
1137   push @buf,['Total New Charges',
1138              $money_char. sprintf("%10.2f",$self->charged) ];
1139   push @buf,['',''];
1140
1141   push @buf,['','-----------'];
1142   push @buf,['Total Charges',
1143              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1144   push @buf,['',''];
1145
1146   #credits
1147   foreach ( $self->cust_credited ) {
1148
1149     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1150
1151     my $reason = substr($_->cust_credit->reason,0,32);
1152     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1153     $reason = " ($reason) " if $reason;
1154     push @buf,[
1155       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1156         $reason,
1157       $money_char. sprintf("%10.2f",$_->amount)
1158     ];
1159   }
1160   #foreach ( @cr_cust_credit ) {
1161   #  push @buf,[
1162   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1163   #    $money_char. sprintf("%10.2f",$_->credited)
1164   #  ];
1165   #}
1166
1167   #get & print payments
1168   foreach ( $self->cust_bill_pay ) {
1169
1170     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1171
1172     push @buf,[
1173       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1174       $money_char. sprintf("%10.2f",$_->amount )
1175     ];
1176   }
1177
1178   #balance due
1179   my $balance_due_msg = $self->balance_due_msg;
1180
1181   push @buf,['','-----------'];
1182   push @buf,[$balance_due_msg, $money_char. 
1183     sprintf("%10.2f", $balance_due ) ];
1184
1185   #create the template
1186   my $templatefile = 'invoice_template';
1187   $templatefile .= "_$template" if $template;
1188   my @invoice_template = $conf->config($templatefile)
1189   or die "cannot load config file $templatefile";
1190   $invoice_lines = 0;
1191   my $wasfunc = 0;
1192   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1193     /invoice_lines\((\d*)\)/;
1194     $invoice_lines += $1 || scalar(@buf);
1195     $wasfunc=1;
1196   }
1197   die "no invoice_lines() functions in template?" unless $wasfunc;
1198   my $invoice_template = new Text::Template (
1199     TYPE   => 'ARRAY',
1200     SOURCE => [ map "$_\n", @invoice_template ],
1201   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1202   $invoice_template->compile()
1203     or die "can't compile template: $Text::Template::ERROR";
1204
1205   #setup template variables
1206   package FS::cust_bill::_template; #!
1207   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1208
1209   $invnum = $self->invnum;
1210   $date = $self->_date;
1211   $page = 1;
1212   $agent = $self->cust_main->agent->agent;
1213
1214   if ( $FS::cust_bill::invoice_lines ) {
1215     $total_pages =
1216       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1217     $total_pages++
1218       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1219   } else {
1220     $total_pages = 1;
1221   }
1222
1223   #format address (variable for the template)
1224   my $l = 0;
1225   @address = ( '', '', '', '', '', '' );
1226   package FS::cust_bill; #!
1227   $FS::cust_bill::_template::address[$l++] =
1228     $cust_main->payname.
1229       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1230         ? " (P.O. #". $cust_main->payinfo. ")"
1231         : ''
1232       )
1233   ;
1234   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1235     if $cust_main->company;
1236   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1237   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1238     if $cust_main->address2;
1239   $FS::cust_bill::_template::address[$l++] =
1240     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1241   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1242     unless $cust_main->country eq 'US';
1243
1244         #  #overdue? (variable for the template)
1245         #  $FS::cust_bill::_template::overdue = ( 
1246         #    $balance_due > 0
1247         #    && $today > $self->_date 
1248         ##    && $self->printed > 1
1249         #    && $self->printed > 0
1250         #  );
1251
1252   #and subroutine for the template
1253   sub FS::cust_bill::_template::invoice_lines {
1254     my $lines = shift || scalar(@buf);
1255     map { 
1256       scalar(@buf) ? shift @buf : [ '', '' ];
1257     }
1258     ( 1 .. $lines );
1259   }
1260
1261   #and fill it in
1262   $FS::cust_bill::_template::page = 1;
1263   my $lines;
1264   my @collect;
1265   while (@buf) {
1266     push @collect, split("\n",
1267       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1268     );
1269     $FS::cust_bill::_template::page++;
1270   }
1271
1272   map "$_\n", @collect;
1273
1274 }
1275
1276 =item print_ps [ TIME [ , TEMPLATE ] ]
1277
1278 Returns an postscript invoice, as a scalar.
1279
1280 TIME an optional value used to control the printing of overdue messages.  The
1281 default is now.  It isn't the date of the invoice; that's the `_date' field.
1282 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1283 L<Time::Local> and L<Date::Parse> for conversion functions.
1284
1285 =cut
1286
1287 #still some false laziness w/print_text
1288 sub print_ps {
1289
1290   my( $self, $today, $template ) = @_;
1291   $today ||= time;
1292
1293 #  my $invnum = $self->invnum;
1294   my $cust_main = $self->cust_main;
1295   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1296     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1297
1298   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1299 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1300   #my $balance_due = $self->owed + $pr_total - $cr_total;
1301   my $balance_due = $self->owed + $pr_total;
1302
1303   #my @collect = ();
1304   #my($description,$amount);
1305   @buf = ();
1306
1307   #create the template
1308   my $templatefile = 'invoice_latex';
1309   $templatefile .= "_$template" if $template;
1310   my @invoice_template = $conf->config($templatefile)
1311     or die "cannot load config file $templatefile";
1312
1313   my %invoice_data = (
1314     'invnum'       => $self->invnum,
1315     'date'         => time2str('%b %o, %Y', $self->_date),
1316     'agent'        => $cust_main->agent->agent,
1317     'payname'      => $cust_main->payname,
1318     'company'      => $cust_main->company,
1319     'address1'     => $cust_main->address1,
1320     'address2'     => $cust_main->address2,
1321     'city'         => $cust_main->city,
1322     'state'        => $cust_main->state,
1323     'zip'          => $cust_main->zip,
1324     'country'      => $cust_main->country,
1325     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1326     'quantity'     => 1,
1327     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1328     'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1329   );
1330
1331   $invoice_data{'footer'} =~ s/\n+$//;
1332   $invoice_data{'notes'} =~ s/\n+$//;
1333
1334   my $countrydefault = $conf->config('countrydefault') || 'US';
1335   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1336
1337   $invoice_data{'po_line'} =
1338     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1339       ? "Purchase Order #". $cust_main->payinfo
1340       : '~';
1341
1342   my @line_item = ();
1343   my @total_item = ();
1344   my @filled_in = ();
1345   while ( @invoice_template ) {
1346     my $line = shift @invoice_template;
1347
1348     if ( $line =~ /^%%Detail\s*$/ ) {
1349
1350       while ( ( my $line_item_line = shift @invoice_template )
1351               !~ /^%%EndDetail\s*$/                            ) {
1352         push @line_item, $line_item_line;
1353       }
1354       foreach my $line_item ( $self->_items ) {
1355       #foreach my $line_item ( $self->_items_pkg ) {
1356         $invoice_data{'ref'} = $line_item->{'pkgnum'};
1357         $invoice_data{'description'} = $line_item->{'description'};
1358         if ( exists $line_item->{'ext_description'} ) {
1359           $invoice_data{'description'} .=
1360             "\\tabularnewline\n~~".
1361             join("\\tabularnewline\n~~", @{$line_item->{'ext_description'}} );
1362         }
1363         $invoice_data{'amount'} = $line_item->{'amount'};
1364         $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1365         push @filled_in,
1366           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1367       }
1368
1369     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1370
1371       while ( ( my $total_item_line = shift @invoice_template )
1372               !~ /^%%EndTotalDetails\s*$/                      ) {
1373         push @total_item, $total_item_line;
1374       }
1375
1376       my @total_fill = ();
1377
1378       my $taxtotal = 0;
1379       foreach my $tax ( $self->_items_tax ) {
1380         $invoice_data{'total_item'} = $tax->{'description'};
1381         $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1382         push @total_fill,
1383           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1384               @total_item;
1385       }
1386
1387       if ( $taxtotal ) {
1388         $invoice_data{'total_item'} = 'Sub-total';
1389         $invoice_data{'total_amount'} =
1390           '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1391         unshift @total_fill,
1392           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1393               @total_item;
1394       }
1395
1396       $invoice_data{'total_item'} = '\textbf{Total}';
1397       $invoice_data{'total_amount'} =
1398         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1399       push @total_fill,
1400         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1401             @total_item;
1402
1403       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1404
1405       # credits
1406       foreach my $credit ( $self->_items_credits ) {
1407         $invoice_data{'total_item'} = $credit->{'description'};
1408         #$credittotal
1409         $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1410         push @total_fill, 
1411           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1412               @total_item;
1413       }
1414
1415       # payments
1416       foreach my $payment ( $self->_items_payments ) {
1417         $invoice_data{'total_item'} = $payment->{'description'};
1418         #$paymenttotal
1419         $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1420         push @total_fill, 
1421           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1422               @total_item;
1423       }
1424
1425       $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1426       $invoice_data{'total_amount'} =
1427         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1428       push @total_fill,
1429         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1430             @total_item;
1431
1432       push @filled_in, @total_fill;
1433
1434     } else {
1435       #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1436       $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1437       push @filled_in, $line;
1438     }
1439
1440   }
1441
1442   sub nounder {
1443     my $var = $1;
1444     $var =~ s/_/\-/g;
1445     $var;
1446   }
1447
1448   my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1449   my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1450
1451   chdir($dir);
1452   my $file = $self->invnum. ".$unique";
1453
1454   open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1455   print TEX join("\n", @filled_in ), "\n";
1456   close TEX;
1457
1458   #error checking!!
1459   system('pslatex', "$file.tex");
1460   system('pslatex', "$file.tex");
1461   #system('dvips', '-t', 'letter', "$file.dvi", "$file.ps");
1462   system('dvips', '-t', 'letter', "$file.dvi" );
1463
1464   open(POSTSCRIPT, "<$file.ps") or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1465
1466   #rm $file.dvi $file.log $file.aux
1467   #unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps");
1468   unlink("$file.dvi", "$file.log", "$file.aux");
1469
1470   my $ps = '';
1471   while (<POSTSCRIPT>) {
1472     $ps .= $_;
1473   }
1474
1475   close POSTSCRIPT;
1476
1477   return $ps;
1478
1479 }
1480
1481 #utility methods for print_*
1482
1483 sub balance_due_msg {
1484   my $self = shift;
1485   my $msg = 'Balance Due';
1486   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1487     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1488   } elsif ( $conf->config('invoice_default_terms') ) {
1489     $msg .= ' - '. $conf->config('invoice_default_terms');
1490   }
1491   $msg;
1492 }
1493
1494 sub _items {
1495   my $self = shift;
1496   my @display = scalar(@_)
1497                 ? @_
1498                 : qw( _items_previous _items_pkg );
1499                 #: qw( _items_pkg );
1500                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1501   my @b = ();
1502   foreach my $display ( @display ) {
1503     push @b, $self->$display(@_);
1504   }
1505   @b;
1506 }
1507
1508 sub _items_previous {
1509   my $self = shift;
1510   my $cust_main = $self->cust_main;
1511   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1512   my @b = ();
1513   foreach ( @pr_cust_bill ) {
1514     push @b, {
1515       'description' => 'Previous Balance, Invoice \#'. $_->invnum. 
1516                        ' ('. time2str('%x',$_->_date). ')',
1517       #'pkgpart'     => 'N/A',
1518       'pkgnum'      => 'N/A',
1519       'amount'      => sprintf("%10.2f", $_->owed),
1520     };
1521   }
1522   @b;
1523
1524   #{
1525   #    'description'     => 'Previous Balance',
1526   #    #'pkgpart'         => 'N/A',
1527   #    'pkgnum'          => 'N/A',
1528   #    'amount'          => sprintf("%10.2f", $pr_total ),
1529   #    'ext_description' => [ map {
1530   #                                 "Invoice ". $_->invnum.
1531   #                                 " (". time2str("%x",$_->_date). ") ".
1532   #                                 sprintf("%10.2f", $_->owed)
1533   #                         } @pr_cust_bill ],
1534
1535   #};
1536 }
1537
1538 sub _items_pkg {
1539   my $self = shift;
1540   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1541   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1542 }
1543
1544 sub _items_tax {
1545   my $self = shift;
1546   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1547   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1548 }
1549
1550 sub _items_cust_bill_pkg {
1551   my $self = shift;
1552   my $cust_bill_pkg = shift;
1553
1554   my @b = ();
1555   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1556
1557     if ( $cust_bill_pkg->pkgnum ) {
1558
1559       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1560       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1561       my $pkg = $part_pkg->pkg;
1562
1563       if ( $cust_bill_pkg->setup != 0 ) {
1564         my $description = $pkg;
1565         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1566         my @d = ();
1567         @d = $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1568         push @b, {
1569           'description'     => $description,
1570           #'pkgpart'         => $part_pkg->pkgpart,
1571           'pkgnum'          => $cust_pkg->pkgnum,
1572           'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
1573           'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1574                                          $cust_pkg->labels        ),
1575                                  @d,
1576                                ],
1577         };
1578       }
1579
1580       if ( $cust_bill_pkg->recur != 0 ) {
1581         push @b, {
1582           'description'     => "$pkg (" .
1583                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1584                                time2str('%x', $cust_bill_pkg->edate). ')',
1585           #'pkgpart'         => $part_pkg->pkgpart,
1586           'pkgnum'          => $cust_pkg->pkgnum,
1587           'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
1588           'ext_description' => [ ( map { $_->[0]. ": ". $_->[1] }
1589                                        $cust_pkg->labels          ),
1590                                  $cust_bill_pkg->details,
1591                                ],
1592         };
1593       }
1594
1595     } else { #pkgnum tax or one-shot line item (??)
1596
1597       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1598                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1599                      : 'Tax';
1600       if ( $cust_bill_pkg->setup != 0 ) {
1601         push @b, {
1602           'description' => $itemdesc,
1603           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1604         };
1605       }
1606       if ( $cust_bill_pkg->recur != 0 ) {
1607         push @b, {
1608           'description' => "$itemdesc (".
1609                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1610                            time2str("%x", $cust_bill_pkg->edate). ')',
1611           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1612         };
1613       }
1614
1615     }
1616
1617   }
1618
1619   @b;
1620
1621 }
1622
1623 sub _items_credits {
1624   my $self = shift;
1625
1626   my @b;
1627   #credits
1628   foreach ( $self->cust_credited ) {
1629
1630     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1631
1632     my $reason = $_->cust_credit->reason;
1633     #my $reason = substr($_->cust_credit->reason,0,32);
1634     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1635     $reason = " ($reason) " if $reason;
1636     push @b, {
1637       #'description' => 'Credit ref\#'. $_->crednum.
1638       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1639       #                 $reason,
1640       'description' => 'Credit applied'.
1641                        time2str("%x",$_->cust_credit->_date). $reason,
1642       'amount'      => sprintf("%10.2f",$_->amount),
1643     };
1644   }
1645   #foreach ( @cr_cust_credit ) {
1646   #  push @buf,[
1647   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1648   #    $money_char. sprintf("%10.2f",$_->credited)
1649   #  ];
1650   #}
1651
1652   @b;
1653
1654 }
1655
1656 sub _items_payments {
1657   my $self = shift;
1658
1659   my @b;
1660   #get & print payments
1661   foreach ( $self->cust_bill_pay ) {
1662
1663     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1664
1665     push @b, {
1666       'description' => "Payment received ".
1667                        time2str("%x",$_->cust_pay->_date ),
1668       'amount'      => sprintf("%10.2f", $_->amount )
1669     };
1670   }
1671
1672   @b;
1673
1674 }
1675
1676 =back
1677
1678 =head1 BUGS
1679
1680 The delete method.
1681
1682 print_text formatting (and some logic :/) is in source, but needs to be
1683 slurped in from a file.  Also number of lines ($=).
1684
1685 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1686 or something similar so the look can be completely customized?)
1687
1688 =head1 SEE ALSO
1689
1690 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1691 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1692 documentation.
1693
1694 =cut
1695
1696 1;
1697