as an invoice event, emailing/printing problems should be fatal and trigger retry
[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 die "(customer # ". $self->custnum. ") can't send invoice email".
421                " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
422                " via server $smtpmachine with SMTP: $!\n";
423
424   }
425
426   if ( $conf->config('invoice_latex') ) {
427     @print_text = $self->print_ps('', $template);
428   }
429
430   if ( grep { $_ eq 'POST' } @invoicing_list ) { #postal
431     open(LPR, "|$lpr")
432       or die "Can't open pipe to $lpr: $!\n";
433     print LPR @print_text;
434     close LPR
435       or die $! ? "Error closing $lpr: $!\n"
436                 : "Exit status $? from $lpr\n";
437   }
438
439   '';
440
441 }
442
443 =item send_csv OPTIONS
444
445 Sends invoice as a CSV data-file to a remote host with the specified protocol.
446
447 Options are:
448
449 protocol - currently only "ftp"
450 server
451 username
452 password
453 dir
454
455 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
456 and YYMMDDHHMMSS is a timestamp.
457
458 The fields of the CSV file is as follows:
459
460 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
461
462 =over 4
463
464 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
465
466 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
467 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
468 fields are filled in.
469
470 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
471 first two fields (B<record_type> and B<invnum>) and the last five fields
472 (B<pkg> through B<edate>) are filled in.
473
474 =item invnum - invoice number
475
476 =item custnum - customer number
477
478 =item _date - invoice date
479
480 =item charged - total invoice amount
481
482 =item first - customer first name
483
484 =item last - customer first name
485
486 =item company - company name
487
488 =item address1 - address line 1
489
490 =item address2 - address line 1
491
492 =item city
493
494 =item state
495
496 =item zip
497
498 =item country
499
500 =item pkg - line item description
501
502 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
503
504 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
505
506 =item sdate - start date for recurring fee
507
508 =item edate - end date for recurring fee
509
510 =back
511
512 =cut
513
514 sub send_csv {
515   my($self, %opt) = @_;
516
517   #part one: create file
518
519   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
520   mkdir $spooldir, 0700 unless -d $spooldir;
521
522   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
523
524   open(CSV, ">$file") or die "can't open $file: $!";
525
526   eval "use Text::CSV_XS";
527   die $@ if $@;
528
529   my $csv = Text::CSV_XS->new({'always_quote'=>1});
530
531   my $cust_main = $self->cust_main;
532
533   $csv->combine(
534     'cust_bill',
535     $self->invnum,
536     $self->custnum,
537     time2str("%x", $self->_date),
538     sprintf("%.2f", $self->charged),
539     ( map { $cust_main->getfield($_) }
540         qw( first last company address1 address2 city state zip country ) ),
541     map { '' } (1..5),
542   ) or die "can't create csv";
543   print CSV $csv->string. "\n";
544
545   #new charges (false laziness w/print_text)
546   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
547
548     my($pkg, $setup, $recur, $sdate, $edate);
549     if ( $cust_bill_pkg->pkgnum ) {
550     
551       ($pkg, $setup, $recur, $sdate, $edate) = (
552         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
553         ( $cust_bill_pkg->setup != 0
554           ? sprintf("%.2f", $cust_bill_pkg->setup )
555           : '' ),
556         ( $cust_bill_pkg->recur != 0
557           ? sprintf("%.2f", $cust_bill_pkg->recur )
558           : '' ),
559         time2str("%x", $cust_bill_pkg->sdate),
560         time2str("%x", $cust_bill_pkg->edate),
561       );
562
563     } else { #pkgnum Tax
564       next unless $cust_bill_pkg->setup != 0;
565       ($pkg, $setup, $recur, $sdate, $edate) =
566         ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
567     }
568
569     $csv->combine(
570       'cust_bill_pkg',
571       $self->invnum,
572       ( map { '' } (1..11) ),
573       ($pkg, $setup, $recur, $sdate, $edate)
574     ) or die "can't create csv";
575     print CSV $csv->string. "\n";
576
577   }
578
579   close CSV or die "can't close CSV: $!";
580
581   #part two: upload it
582
583   my $net;
584   if ( $opt{protocol} eq 'ftp' ) {
585     eval "use Net::FTP;";
586     die $@ if $@;
587     $net = Net::FTP->new($opt{server}) or die @$;
588   } else {
589     die "unknown protocol: $opt{protocol}";
590   }
591
592   $net->login( $opt{username}, $opt{password} )
593     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
594
595   $net->binary or die "can't set binary mode";
596
597   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
598
599   $net->put($file) or die "can't put $file: $!";
600
601   $net->quit;
602
603   unlink $file;
604
605 }
606
607 =item comp
608
609 Pays this invoice with a compliemntary payment.  If there is an error,
610 returns the error, otherwise returns false.
611
612 =cut
613
614 sub comp {
615   my $self = shift;
616   my $cust_pay = new FS::cust_pay ( {
617     'invnum'   => $self->invnum,
618     'paid'     => $self->owed,
619     '_date'    => '',
620     'payby'    => 'COMP',
621     'payinfo'  => $self->cust_main->payinfo,
622     'paybatch' => '',
623   } );
624   $cust_pay->insert;
625 }
626
627 =item realtime_card
628
629 Attempts to pay this invoice with a credit card payment via a
630 Business::OnlinePayment realtime gateway.  See
631 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
632 for supported processors.
633
634 =cut
635
636 sub realtime_card {
637   my $self = shift;
638   $self->realtime_bop(
639     'CC',
640     $bop_processor,
641     $bop_login,
642     $bop_password,
643     $bop_action,
644     \@bop_options,
645     @_
646   );
647 }
648
649 =item realtime_ach
650
651 Attempts to pay this invoice with an electronic check (ACH) payment via a
652 Business::OnlinePayment realtime gateway.  See
653 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
654 for supported processors.
655
656 =cut
657
658 sub realtime_ach {
659   my $self = shift;
660   $self->realtime_bop(
661     'ECHECK',
662     $ach_processor,
663     $ach_login,
664     $ach_password,
665     $ach_action,
666     \@ach_options,
667     @_
668   );
669 }
670
671 =item realtime_lec
672
673 Attempts to pay this invoice with phone bill (LEC) payment via a
674 Business::OnlinePayment realtime gateway.  See
675 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
676 for supported processors.
677
678 =cut
679
680 sub realtime_lec {
681   my $self = shift;
682   $self->realtime_bop(
683     'LEC',
684     $bop_processor,
685     $bop_login,
686     $bop_password,
687     $bop_action,
688     \@bop_options,
689     @_
690   );
691 }
692
693 sub realtime_bop {
694   my( $self, $method, $processor, $login, $password, $action, $options ) = @_;
695
696   #trim an extraneous blank line
697   pop @$options if scalar(@$options) % 2 && $options->[-1] =~ /^\s*$/;
698
699   my $cust_main = $self->cust_main;
700   my $balance = $cust_main->balance;
701   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
702   $amount = sprintf("%.2f", $amount);
703   return "not run (balance $balance)" unless $amount > 0;
704
705   my $address = $cust_main->address1;
706   $address .= ", ". $cust_main->address2 if $cust_main->address2;
707
708   my($payname, $payfirst, $paylast);
709   if ( $cust_main->payname && $method ne 'ECHECK' ) {
710     $payname = $cust_main->payname;
711     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
712       or do {
713               #$dbh->rollback if $oldAutoCommit;
714               return "Illegal payname $payname";
715             };
716     ($payfirst, $paylast) = ($1, $2);
717   } else {
718     $payfirst = $cust_main->getfield('first');
719     $paylast = $cust_main->getfield('last');
720     $payname =  "$payfirst $paylast";
721   }
722
723   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
724   if ( $conf->exists('emailinvoiceauto')
725        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
726     push @invoicing_list, $cust_main->all_emails;
727   }
728   my $email = $invoicing_list[0];
729
730   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
731
732   my $description = 'Internet Services';
733   if ( $conf->exists('business-onlinepayment-description') ) {
734     my $dtempl = $conf->config('business-onlinepayment-description');
735
736     my $agent_obj = $cust_main->agent
737       or die "can't retreive agent for $cust_main (agentnum ".
738              $cust_main->agentnum. ")";
739     my $agent = $agent_obj->agent;
740     my $pkgs = join(', ',
741       map { $_->cust_pkg->part_pkg->pkg }
742         grep { $_->pkgnum } $self->cust_bill_pkg
743     );
744     $description = eval qq("$dtempl");
745
746   }
747
748   my %content;
749   if ( $method eq 'CC' ) { 
750
751     $content{card_number} = $cust_main->payinfo;
752     $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
753     $content{expiration} = "$2/$1";
754
755     $content{cvv2} = $cust_main->paycvv
756       if defined $cust_main->dbdef_table->column('paycvv')
757          && length($cust_main->paycvv);
758
759     $content{recurring_billing} = 'YES'
760       if qsearch('cust_pay', { 'custnum' => $cust_main->custnum,
761                                'payby'   => 'CARD',
762                                'payinfo' => $cust_main->payinfo, } );
763
764   } elsif ( $method eq 'ECHECK' ) {
765     my($account_number,$routing_code) = $cust_main->payinfo;
766     ( $content{account_number}, $content{routing_code} ) =
767       split('@', $cust_main->payinfo);
768     $content{bank_name} = $cust_main->payname;
769     $content{account_type} = 'CHECKING';
770     $content{account_name} = $payname;
771     $content{customer_org} = $self->company ? 'B' : 'I';
772     $content{customer_ssn} = $self->ss;
773   } elsif ( $method eq 'LEC' ) {
774     $content{phone} = $cust_main->payinfo;
775   }
776   
777   my $transaction =
778     new Business::OnlinePayment( $processor, @$options );
779   $transaction->content(
780     'type'           => $method,
781     'login'          => $login,
782     'password'       => $password,
783     'action'         => $action1,
784     'description'    => $description,
785     'amount'         => $amount,
786     'invoice_number' => $self->invnum,
787     'customer_id'    => $self->custnum,
788     'last_name'      => $paylast,
789     'first_name'     => $payfirst,
790     'name'           => $payname,
791     'address'        => $address,
792     'city'           => $cust_main->city,
793     'state'          => $cust_main->state,
794     'zip'            => $cust_main->zip,
795     'country'        => $cust_main->country,
796     'referer'        => 'http://cleanwhisker.420.am/',
797     'email'          => $email,
798     'phone'          => $cust_main->daytime || $cust_main->night,
799     %content, #after
800   );
801   $transaction->submit();
802
803   if ( $transaction->is_success() && $action2 ) {
804     my $auth = $transaction->authorization;
805     my $ordernum = $transaction->can('order_number')
806                    ? $transaction->order_number
807                    : '';
808
809     #warn "********* $auth ***********\n";
810     #warn "********* $ordernum ***********\n";
811     my $capture =
812       new Business::OnlinePayment( $processor, @$options );
813
814     my %capture = (
815       %content,
816       type           => $method,
817       action         => $action2,
818       login          => $login,
819       password       => $password,
820       order_number   => $ordernum,
821       amount         => $amount,
822       authorization  => $auth,
823       description    => $description,
824     );
825
826     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
827                            transaction_sequence_num local_transaction_date    
828                            local_transaction_time AVS_result_code          )) {
829       $capture{$field} = $transaction->$field() if $transaction->can($field);
830     }
831
832     $capture->content( %capture );
833
834     $capture->submit();
835
836     unless ( $capture->is_success ) {
837       my $e = "Authorization sucessful but capture failed, invnum #".
838               $self->invnum. ': '.  $capture->result_code.
839               ": ". $capture->error_message;
840       warn $e;
841       return $e;
842     }
843
844   }
845
846   #remove paycvv after initial transaction
847   #make this disable-able via a config option if anyone insists?  
848   # (though that probably violates cardholder agreements)
849   use Business::CreditCard;
850   if ( defined $cust_main->dbdef_table->column('paycvv')
851        && length($cust_main->paycvv)
852        && ! grep { $_ eq cardtype($cust_main->payinfo) } $conf->config('cvv-save')
853
854   ) {
855     my $new = new FS::cust_main { $cust_main->hash };
856     $new->paycvv('');
857     my $error = $new->replace($cust_main);
858     if ( $error ) {
859       warn "error removing cvv: $error\n";
860     }
861   }
862
863   #result handling
864   if ( $transaction->is_success() ) {
865
866     my %method2payby = (
867       'CC'     => 'CARD',
868       'ECHECK' => 'CHEK',
869       'LEC'    => 'LECB',
870     );
871
872     my $cust_pay = new FS::cust_pay ( {
873        'invnum'   => $self->invnum,
874        'paid'     => $amount,
875        '_date'     => '',
876        'payby'    => $method2payby{$method},
877        'payinfo'  => $cust_main->payinfo,
878        'paybatch' => "$processor:". $transaction->authorization,
879     } );
880     my $error = $cust_pay->insert;
881     if ( $error ) {
882       $cust_pay->invnum(''); #try again with no specific invnum
883       my $error2 = $cust_pay->insert;
884       if ( $error2 ) {
885         # gah, even with transactions.
886         my $e = 'WARNING: Card/ACH debited but database not updated - '.
887                 "error inserting payment ($processor): $error2".
888                 ' (previously tried insert with invnum #' . $self->invnum.
889                 ": $error )";
890         warn $e;
891         return $e;
892       }
893     }
894     return ''; #no error
895
896   #} elsif ( $options{'report_badcard'} ) {
897   } else {
898
899     my $perror = "$processor error, invnum #". $self->invnum. ': '.
900                  $transaction->result_code. ": ". $transaction->error_message;
901
902     if ( !$realtime_bop_decline_quiet && $conf->exists('emaildecline')
903          && grep { $_ ne 'POST' } $cust_main->invoicing_list
904          && ! grep { $transaction->error_message =~ /$_/ }
905                    $conf->config('emaildecline-exclude')
906     ) {
907       my @templ = $conf->config('declinetemplate');
908       my $template = new Text::Template (
909         TYPE   => 'ARRAY',
910         SOURCE => [ map "$_\n", @templ ],
911       ) or return "($perror) can't create template: $Text::Template::ERROR";
912       $template->compile()
913         or return "($perror) can't compile template: $Text::Template::ERROR";
914
915       my $templ_hash = { error => $transaction->error_message };
916
917       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
918       $ENV{MAILADDRESS} = $invoice_from;
919       my $header = new Mail::Header ( [
920         "From: $invoice_from",
921         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
922         "Sender: $invoice_from",
923         "Reply-To: $invoice_from",
924         "Date: ". time2str("%a, %d %b %Y %X %z", time),
925         "Subject: Your payment could not be processed",
926       ] );
927       my $message = new Mail::Internet (
928         'Header' => $header,
929         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
930       );
931       $!=0;
932       $message->smtpsend( Host => $smtpmachine )
933         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
934           or return "($perror) (customer # ". $self->custnum.
935             ") can't send card decline email to ".
936             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
937             " via server $smtpmachine with SMTP: $!";
938     }
939   
940     return $perror;
941   }
942
943 }
944
945 =item realtime_card_cybercash
946
947 Attempts to pay this invoice with the CyberCash CashRegister realtime gateway.
948
949 =cut
950
951 sub realtime_card_cybercash {
952   my $self = shift;
953   my $cust_main = $self->cust_main;
954   my $amount = $self->owed;
955
956   return "CyberCash CashRegister real-time card processing not enabled!"
957     unless $cybercash eq 'cybercash3.2';
958
959   my $address = $cust_main->address1;
960   $address .= ", ". $cust_main->address2 if $cust_main->address2;
961
962   #fix exp. date
963   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
964   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
965   my $exp = "$2/$1";
966
967   #
968
969   my $paybatch = $self->invnum. 
970                   '-' . time2str("%y%m%d%H%M%S", time);
971
972   my $payname = $cust_main->payname ||
973                 $cust_main->getfield('first').' '.$cust_main->getfield('last');
974
975   my $country = $cust_main->country eq 'US' ? 'USA' : $cust_main->country;
976
977   my @full_xaction = ( $xaction,
978     'Order-ID'     => $paybatch,
979     'Amount'       => "usd $amount",
980     'Card-Number'  => $cust_main->getfield('payinfo'),
981     'Card-Name'    => $payname,
982     'Card-Address' => $address,
983     'Card-City'    => $cust_main->getfield('city'),
984     'Card-State'   => $cust_main->getfield('state'),
985     'Card-Zip'     => $cust_main->getfield('zip'),
986     'Card-Country' => $country,
987     'Card-Exp'     => $exp,
988   );
989
990   my %result;
991   %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
992   
993   if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
994     my $cust_pay = new FS::cust_pay ( {
995        'invnum'   => $self->invnum,
996        'paid'     => $amount,
997        '_date'     => '',
998        'payby'    => 'CARD',
999        'payinfo'  => $cust_main->payinfo,
1000        'paybatch' => "$cybercash:$paybatch",
1001     } );
1002     my $error = $cust_pay->insert;
1003     if ( $error ) {
1004       # gah, even with transactions.
1005       my $e = 'WARNING: Card debited but database not updated - '.
1006               'error applying payment, invnum #' . $self->invnum.
1007               " (CyberCash Order-ID $paybatch): $error";
1008       warn $e;
1009       return $e;
1010     } else {
1011       return '';
1012     }
1013 #  } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1014 #            || $options{'report_badcard'}
1015 #          ) {
1016   } else {
1017      return 'Cybercash error, invnum #' . 
1018        $self->invnum. ':'. $result{'MErrMsg'};
1019   }
1020
1021 }
1022
1023 =item batch_card
1024
1025 Adds a payment for this invoice to the pending credit card batch (see
1026 L<FS::cust_pay_batch>).
1027
1028 =cut
1029
1030 sub batch_card {
1031   my $self = shift;
1032   my $cust_main = $self->cust_main;
1033
1034   my $cust_pay_batch = new FS::cust_pay_batch ( {
1035     'invnum'   => $self->getfield('invnum'),
1036     'custnum'  => $cust_main->getfield('custnum'),
1037     'last'     => $cust_main->getfield('last'),
1038     'first'    => $cust_main->getfield('first'),
1039     'address1' => $cust_main->getfield('address1'),
1040     'address2' => $cust_main->getfield('address2'),
1041     'city'     => $cust_main->getfield('city'),
1042     'state'    => $cust_main->getfield('state'),
1043     'zip'      => $cust_main->getfield('zip'),
1044     'country'  => $cust_main->getfield('country'),
1045     'cardnum'  => $cust_main->getfield('payinfo'),
1046     'exp'      => $cust_main->getfield('paydate'),
1047     'payname'  => $cust_main->getfield('payname'),
1048     'amount'   => $self->owed,
1049   } );
1050   my $error = $cust_pay_batch->insert;
1051   die $error if $error;
1052
1053   '';
1054 }
1055
1056 =item print_text [ TIME [ , TEMPLATE ] ]
1057
1058 Returns an text invoice, as a list of lines.
1059
1060 TIME an optional value used to control the printing of overdue messages.  The
1061 default is now.  It isn't the date of the invoice; that's the `_date' field.
1062 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1063 L<Time::Local> and L<Date::Parse> for conversion functions.
1064
1065 =cut
1066
1067 sub print_text {
1068
1069   my( $self, $today, $template ) = @_;
1070   $today ||= time;
1071 #  my $invnum = $self->invnum;
1072   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
1073   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1074     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1075
1076   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1077 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1078   #my $balance_due = $self->owed + $pr_total - $cr_total;
1079   my $balance_due = $self->owed + $pr_total;
1080
1081   #my @collect = ();
1082   #my($description,$amount);
1083   @buf = ();
1084
1085   #previous balance
1086   foreach ( @pr_cust_bill ) {
1087     push @buf, [
1088       "Previous Balance, Invoice #". $_->invnum. 
1089                  " (". time2str("%x",$_->_date). ")",
1090       $money_char. sprintf("%10.2f",$_->owed)
1091     ];
1092   }
1093   if (@pr_cust_bill) {
1094     push @buf,['','-----------'];
1095     push @buf,[ 'Total Previous Balance',
1096                 $money_char. sprintf("%10.2f",$pr_total ) ];
1097     push @buf,['',''];
1098   }
1099
1100   #new charges
1101   foreach my $cust_bill_pkg (
1102     ( grep {   $_->pkgnum } $self->cust_bill_pkg ),  #packages first
1103     ( grep { ! $_->pkgnum } $self->cust_bill_pkg ),  #then taxes
1104   ) {
1105
1106     if ( $cust_bill_pkg->pkgnum ) {
1107
1108       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1109       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1110       my $pkg = $part_pkg->pkg;
1111
1112       if ( $cust_bill_pkg->setup != 0 ) {
1113         my $description = $pkg;
1114         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1115         push @buf, [ $description,
1116                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1117         push @buf,
1118           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1119       }
1120
1121       if ( $cust_bill_pkg->recur != 0 ) {
1122         push @buf, [
1123           "$pkg (" . time2str("%x", $cust_bill_pkg->sdate) . " - " .
1124                                 time2str("%x", $cust_bill_pkg->edate) . ")",
1125           $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1126         ];
1127         push @buf,
1128           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
1129       }
1130
1131     } else { #pkgnum tax or one-shot line item
1132       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1133                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1134                      : 'Tax';
1135       if ( $cust_bill_pkg->setup != 0 ) {
1136         push @buf, [ $itemdesc,
1137                      $money_char. sprintf("%10.2f", $cust_bill_pkg->setup) ];
1138       }
1139       if ( $cust_bill_pkg->recur != 0 ) {
1140         push @buf, [ "$itemdesc (". time2str("%x", $cust_bill_pkg->sdate). " - "
1141                                   . time2str("%x", $cust_bill_pkg->edate). ")",
1142                      $money_char. sprintf("%10.2f", $cust_bill_pkg->recur)
1143                    ];
1144       }
1145     }
1146   }
1147
1148   push @buf,['','-----------'];
1149   push @buf,['Total New Charges',
1150              $money_char. sprintf("%10.2f",$self->charged) ];
1151   push @buf,['',''];
1152
1153   push @buf,['','-----------'];
1154   push @buf,['Total Charges',
1155              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
1156   push @buf,['',''];
1157
1158   #credits
1159   foreach ( $self->cust_credited ) {
1160
1161     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1162
1163     my $reason = substr($_->cust_credit->reason,0,32);
1164     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
1165     $reason = " ($reason) " if $reason;
1166     push @buf,[
1167       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
1168         $reason,
1169       $money_char. sprintf("%10.2f",$_->amount)
1170     ];
1171   }
1172   #foreach ( @cr_cust_credit ) {
1173   #  push @buf,[
1174   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1175   #    $money_char. sprintf("%10.2f",$_->credited)
1176   #  ];
1177   #}
1178
1179   #get & print payments
1180   foreach ( $self->cust_bill_pay ) {
1181
1182     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1183
1184     push @buf,[
1185       "Payment received ". time2str("%x",$_->cust_pay->_date ),
1186       $money_char. sprintf("%10.2f",$_->amount )
1187     ];
1188   }
1189
1190   #balance due
1191   my $balance_due_msg = $self->balance_due_msg;
1192
1193   push @buf,['','-----------'];
1194   push @buf,[$balance_due_msg, $money_char. 
1195     sprintf("%10.2f", $balance_due ) ];
1196
1197   #create the template
1198   my $templatefile = 'invoice_template';
1199   $templatefile .= "_$template" if $template;
1200   my @invoice_template = $conf->config($templatefile)
1201   or die "cannot load config file $templatefile";
1202   $invoice_lines = 0;
1203   my $wasfunc = 0;
1204   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
1205     /invoice_lines\((\d*)\)/;
1206     $invoice_lines += $1 || scalar(@buf);
1207     $wasfunc=1;
1208   }
1209   die "no invoice_lines() functions in template?" unless $wasfunc;
1210   my $invoice_template = new Text::Template (
1211     TYPE   => 'ARRAY',
1212     SOURCE => [ map "$_\n", @invoice_template ],
1213   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
1214   $invoice_template->compile()
1215     or die "can't compile template: $Text::Template::ERROR";
1216
1217   #setup template variables
1218   package FS::cust_bill::_template; #!
1219   use vars qw( $invnum $date $page $total_pages @address $overdue @buf $agent );
1220
1221   $invnum = $self->invnum;
1222   $date = $self->_date;
1223   $page = 1;
1224   $agent = $self->cust_main->agent->agent;
1225
1226   if ( $FS::cust_bill::invoice_lines ) {
1227     $total_pages =
1228       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
1229     $total_pages++
1230       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
1231   } else {
1232     $total_pages = 1;
1233   }
1234
1235   #format address (variable for the template)
1236   my $l = 0;
1237   @address = ( '', '', '', '', '', '' );
1238   package FS::cust_bill; #!
1239   $FS::cust_bill::_template::address[$l++] =
1240     $cust_main->payname.
1241       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
1242         ? " (P.O. #". $cust_main->payinfo. ")"
1243         : ''
1244       )
1245   ;
1246   $FS::cust_bill::_template::address[$l++] = $cust_main->company
1247     if $cust_main->company;
1248   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
1249   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
1250     if $cust_main->address2;
1251   $FS::cust_bill::_template::address[$l++] =
1252     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
1253   $FS::cust_bill::_template::address[$l++] = $cust_main->country
1254     unless $cust_main->country eq 'US';
1255
1256         #  #overdue? (variable for the template)
1257         #  $FS::cust_bill::_template::overdue = ( 
1258         #    $balance_due > 0
1259         #    && $today > $self->_date 
1260         ##    && $self->printed > 1
1261         #    && $self->printed > 0
1262         #  );
1263
1264   #and subroutine for the template
1265   sub FS::cust_bill::_template::invoice_lines {
1266     my $lines = shift || scalar(@buf);
1267     map { 
1268       scalar(@buf) ? shift @buf : [ '', '' ];
1269     }
1270     ( 1 .. $lines );
1271   }
1272
1273   #and fill it in
1274   $FS::cust_bill::_template::page = 1;
1275   my $lines;
1276   my @collect;
1277   while (@buf) {
1278     push @collect, split("\n",
1279       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1280     );
1281     $FS::cust_bill::_template::page++;
1282   }
1283
1284   map "$_\n", @collect;
1285
1286 }
1287
1288 =item print_latex [ TIME [ , TEMPLATE ] ]
1289
1290 Internal method - returns a filename of a filled-in LaTeX template for this
1291 invoice (Note: add ".tex" to get the actual filename).
1292
1293 See print_ps and print_pdf for methods that return PostScript and PDF output.
1294
1295 TIME an optional value used to control the printing of overdue messages.  The
1296 default is now.  It isn't the date of the invoice; that's the `_date' field.
1297 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1298 L<Time::Local> and L<Date::Parse> for conversion functions.
1299
1300 =cut
1301
1302 #still some false laziness w/print_text
1303 sub print_latex {
1304
1305   my( $self, $today, $template ) = @_;
1306   $today ||= time;
1307
1308 #  my $invnum = $self->invnum;
1309   my $cust_main = $self->cust_main;
1310   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1311     unless $cust_main->payname && $cust_main->payby ne 'CHEK';
1312
1313   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1314 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
1315   #my $balance_due = $self->owed + $pr_total - $cr_total;
1316   my $balance_due = $self->owed + $pr_total;
1317
1318   #my @collect = ();
1319   #my($description,$amount);
1320   @buf = ();
1321
1322   #create the template
1323   my $templatefile = 'invoice_latex';
1324   $templatefile .= "_$template" if $template;
1325   my @invoice_template = $conf->config($templatefile)
1326     or die "cannot load config file $templatefile";
1327
1328   my %invoice_data = (
1329     'invnum'       => $self->invnum,
1330     'date'         => time2str('%b %o, %Y', $self->_date),
1331     'agent'        => _latex_escape($cust_main->agent->agent),
1332     'payname'      => _latex_escape($cust_main->payname),
1333     'company'      => _latex_escape($cust_main->company),
1334     'address1'     => _latex_escape($cust_main->address1),
1335     'address2'     => _latex_escape($cust_main->address2),
1336     'city'         => _latex_escape($cust_main->city),
1337     'state'        => _latex_escape($cust_main->state),
1338     'zip'          => _latex_escape($cust_main->zip),
1339     'country'      => _latex_escape($cust_main->country),
1340     'footer'       => join("\n", $conf->config('invoice_latexfooter') ),
1341     'smallfooter'  => join("\n", $conf->config('invoice_latexsmallfooter') ),
1342     'quantity'     => 1,
1343     'terms'        => $conf->config('invoice_default_terms') || 'Payable upon receipt',
1344     #'notes'        => join("\n", $conf->config('invoice_latexnotes') ),
1345   );
1346
1347   my $countrydefault = $conf->config('countrydefault') || 'US';
1348   $invoice_data{'country'} = '' if $invoice_data{'country'} eq $countrydefault;
1349
1350   #do variable substitutions in notes
1351   $invoice_data{'notes'} =
1352     join("\n",
1353       map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1354         $conf->config('invoice_latexnotes')
1355     );
1356
1357   $invoice_data{'footer'} =~ s/\n+$//;
1358   $invoice_data{'smallfooter'} =~ s/\n+$//;
1359   $invoice_data{'notes'} =~ s/\n+$//;
1360
1361   $invoice_data{'po_line'} =
1362     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
1363       ? _latex_escape("Purchase Order #". $cust_main->payinfo)
1364       : '~';
1365
1366   my @line_item = ();
1367   my @total_item = ();
1368   my @filled_in = ();
1369   while ( @invoice_template ) {
1370     my $line = shift @invoice_template;
1371
1372     if ( $line =~ /^%%Detail\s*$/ ) {
1373
1374       while ( ( my $line_item_line = shift @invoice_template )
1375               !~ /^%%EndDetail\s*$/                            ) {
1376         push @line_item, $line_item_line;
1377       }
1378       foreach my $line_item ( $self->_items ) {
1379       #foreach my $line_item ( $self->_items_pkg ) {
1380         $invoice_data{'ref'} = $line_item->{'pkgnum'};
1381         $invoice_data{'description'} = _latex_escape($line_item->{'description'});
1382         if ( exists $line_item->{'ext_description'} ) {
1383           $invoice_data{'description'} .=
1384             "\\tabularnewline\n~~".
1385             join("\\tabularnewline\n~~", map { _latex_escape($_) } @{$line_item->{'ext_description'}} );
1386         }
1387         $invoice_data{'amount'} = $line_item->{'amount'};
1388         $invoice_data{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
1389         push @filled_in,
1390           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b } @line_item;
1391       }
1392
1393     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
1394
1395       while ( ( my $total_item_line = shift @invoice_template )
1396               !~ /^%%EndTotalDetails\s*$/                      ) {
1397         push @total_item, $total_item_line;
1398       }
1399
1400       my @total_fill = ();
1401
1402       my $taxtotal = 0;
1403       foreach my $tax ( $self->_items_tax ) {
1404         $invoice_data{'total_item'} = _latex_escape($tax->{'description'});
1405         $taxtotal += ( $invoice_data{'total_amount'} = $tax->{'amount'} );
1406         push @total_fill,
1407           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1408               @total_item;
1409       }
1410
1411       if ( $taxtotal ) {
1412         $invoice_data{'total_item'} = 'Sub-total';
1413         $invoice_data{'total_amount'} =
1414           '\dollar '. sprintf('%.2f', $self->charged - $taxtotal );
1415         unshift @total_fill,
1416           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1417               @total_item;
1418       }
1419
1420       $invoice_data{'total_item'} = '\textbf{Total}';
1421       $invoice_data{'total_amount'} =
1422         '\textbf{\dollar '. sprintf('%.2f', $self->charged + $pr_total ). '}';
1423       push @total_fill,
1424         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1425             @total_item;
1426
1427       #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
1428
1429       # credits
1430       foreach my $credit ( $self->_items_credits ) {
1431         $invoice_data{'total_item'} = _latex_escape($credit->{'description'});
1432         #$credittotal
1433         $invoice_data{'total_amount'} = '-\dollar '. $credit->{'amount'};
1434         push @total_fill, 
1435           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1436               @total_item;
1437       }
1438
1439       # payments
1440       foreach my $payment ( $self->_items_payments ) {
1441         $invoice_data{'total_item'} = _latex_escape($payment->{'description'});
1442         #$paymenttotal
1443         $invoice_data{'total_amount'} = '-\dollar '. $payment->{'amount'};
1444         push @total_fill, 
1445           map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1446               @total_item;
1447       }
1448
1449       $invoice_data{'total_item'} = '\textbf{'. $self->balance_due_msg. '}';
1450       $invoice_data{'total_amount'} =
1451         '\textbf{\dollar '. sprintf('%.2f', $self->owed + $pr_total ). '}';
1452       push @total_fill,
1453         map { my $b=$_; $b =~ s/\$(\w+)/$invoice_data{$1}/eg; $b }
1454             @total_item;
1455
1456       push @filled_in, @total_fill;
1457
1458     } else {
1459       #$line =~ s/\$(\w+)/$invoice_data{$1}/eg;
1460       $line =~ s/\$(\w+)/exists($invoice_data{$1}) ? $invoice_data{$1} : nounder($1)/eg;
1461       push @filled_in, $line;
1462     }
1463
1464   }
1465
1466   sub nounder {
1467     my $var = $1;
1468     $var =~ s/_/\-/g;
1469     $var;
1470   }
1471
1472   my $dir = '/tmp'; #! /usr/local/etc/freeside/invoices.datasrc/
1473   my $unique = int(rand(2**31)); #UGH... use File::Temp or something
1474
1475   chdir($dir);
1476   my $file = $self->invnum. ".$unique";
1477
1478   open(TEX,">$file.tex") or die "can't open $file.tex: $!\n";
1479   print TEX join("\n", @filled_in ), "\n";
1480   close TEX;
1481
1482   return $file;
1483
1484 }
1485
1486 =item print_ps [ TIME [ , TEMPLATE ] ]
1487
1488 Returns an postscript invoice, as a scalar.
1489
1490 TIME an optional value used to control the printing of overdue messages.  The
1491 default is now.  It isn't the date of the invoice; that's the `_date' field.
1492 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1493 L<Time::Local> and L<Date::Parse> for conversion functions.
1494
1495 =cut
1496
1497 sub print_ps {
1498   my $self = shift;
1499
1500   my $file = $self->print_latex(@_);
1501
1502   #error checking!!
1503   system('pslatex', "$file.tex");
1504   system('pslatex', "$file.tex");
1505   system('dvips', '-q', '-t', 'letter', "$file.dvi", '-o', "$file.ps" );
1506
1507   open(POSTSCRIPT, "<$file.ps")
1508     or die "can't open $file.ps (probable error in LaTeX template): $!\n";
1509
1510   unlink("$file.dvi", "$file.log", "$file.aux", "$file.ps", "$file.tex");
1511
1512   my $ps = '';
1513   while (<POSTSCRIPT>) {
1514     $ps .= $_;
1515   }
1516
1517   close POSTSCRIPT;
1518
1519   return $ps;
1520
1521 }
1522
1523 =item print_pdf [ TIME [ , TEMPLATE ] ]
1524
1525 Returns an PDF invoice, as a scalar.
1526
1527 TIME an optional value used to control the printing of overdue messages.  The
1528 default is now.  It isn't the date of the invoice; that's the `_date' field.
1529 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1530 L<Time::Local> and L<Date::Parse> for conversion functions.
1531
1532 =cut
1533
1534 sub print_pdf {
1535   my $self = shift;
1536
1537   my $file = $self->print_latex(@_);
1538
1539   #system('pdflatex', "$file.tex");
1540   #system('pdflatex', "$file.tex");
1541   #! LaTeX Error: Unknown graphics extension: .eps.
1542
1543   #error checking!!
1544   system('pslatex', "$file.tex");
1545   system('pslatex', "$file.tex");
1546
1547   #system('dvipdf', "$file.dvi", "$file.pdf" );
1548   system("dvips -q -t letter -f $file.dvi | gs -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -sOutputFile=$file.pdf -c save pop -");
1549
1550   open(PDF, "<$file.pdf")
1551     or die "can't open $file.pdf (probably error in LaTeX tempalte: $!\n";
1552
1553   unlink("$file.dvi", "$file.log", "$file.aux", "$file.pdf", "$file.tex");
1554
1555   my $pdf = '';
1556   while (<PDF>) {
1557     $pdf .= $_;
1558   }
1559
1560   close PDF;
1561
1562   return $pdf;
1563
1564 }
1565
1566 # quick subroutine for print_latex
1567 #
1568 # There are ten characters that LaTeX treats as special characters, which
1569 # means that they do not simply typeset themselves: 
1570 #      # $ % & ~ _ ^ \ { }
1571 #
1572 # TeX ignores blanks following an escaped character; if you want a blank (as
1573 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
1574
1575 sub _latex_escape {
1576   my $value = shift;
1577   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( length($2) ? "\\$2" : '' )/ge;
1578   $value;
1579 }
1580
1581 #utility methods for print_*
1582
1583 sub balance_due_msg {
1584   my $self = shift;
1585   my $msg = 'Balance Due';
1586   return $msg unless $conf->exists('invoice_default_terms');
1587   if ( $conf->config('invoice_default_terms') =~ /^\s*Net\s*(\d+)\s*$/ ) {
1588     $msg .= ' - Please pay by '. time2str("%x", $self->_date + ($1*86400) );
1589   } elsif ( $conf->config('invoice_default_terms') ) {
1590     $msg .= ' - '. $conf->config('invoice_default_terms');
1591   }
1592   $msg;
1593 }
1594
1595 sub _items {
1596   my $self = shift;
1597   my @display = scalar(@_)
1598                 ? @_
1599                 : qw( _items_previous _items_pkg );
1600                 #: qw( _items_pkg );
1601                 #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
1602   my @b = ();
1603   foreach my $display ( @display ) {
1604     push @b, $self->$display(@_);
1605   }
1606   @b;
1607 }
1608
1609 sub _items_previous {
1610   my $self = shift;
1611   my $cust_main = $self->cust_main;
1612   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
1613   my @b = ();
1614   foreach ( @pr_cust_bill ) {
1615     push @b, {
1616       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
1617                        ' ('. time2str('%x',$_->_date). ')',
1618       #'pkgpart'     => 'N/A',
1619       'pkgnum'      => 'N/A',
1620       'amount'      => sprintf("%10.2f", $_->owed),
1621     };
1622   }
1623   @b;
1624
1625   #{
1626   #    'description'     => 'Previous Balance',
1627   #    #'pkgpart'         => 'N/A',
1628   #    'pkgnum'          => 'N/A',
1629   #    'amount'          => sprintf("%10.2f", $pr_total ),
1630   #    'ext_description' => [ map {
1631   #                                 "Invoice ". $_->invnum.
1632   #                                 " (". time2str("%x",$_->_date). ") ".
1633   #                                 sprintf("%10.2f", $_->owed)
1634   #                         } @pr_cust_bill ],
1635
1636   #};
1637 }
1638
1639 sub _items_pkg {
1640   my $self = shift;
1641   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
1642   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1643 }
1644
1645 sub _items_tax {
1646   my $self = shift;
1647   my @cust_bill_pkg = grep { ! $_->pkgnum } $self->cust_bill_pkg;
1648   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
1649 }
1650
1651 sub _items_cust_bill_pkg {
1652   my $self = shift;
1653   my $cust_bill_pkg = shift;
1654
1655   my @b = ();
1656   foreach my $cust_bill_pkg ( @$cust_bill_pkg ) {
1657
1658     if ( $cust_bill_pkg->pkgnum ) {
1659
1660       my $cust_pkg = qsearchs('cust_pkg', { pkgnum =>$cust_bill_pkg->pkgnum } );
1661       my $part_pkg = qsearchs('part_pkg', { pkgpart=>$cust_pkg->pkgpart } );
1662       my $pkg = $part_pkg->pkg;
1663
1664       my %labels;
1665       #tie %labels, 'Tie::IxHash';
1666       push @{ $labels{$_->[0]} }, $_->[1] foreach $cust_pkg->labels;
1667       my @ext_description;
1668       foreach my $label ( keys %labels ) {
1669         my @values = @{ $labels{$label} };
1670         my $num = scalar(@values);
1671         if ( $num > 5 ) {
1672           push @ext_description, "$label ($num)";
1673         } else {
1674           push @ext_description, map { "$label: $_" } @values;
1675         }
1676       }
1677
1678       if ( $cust_bill_pkg->setup != 0 ) {
1679         my $description = $pkg;
1680         $description .= ' Setup' if $cust_bill_pkg->recur != 0;
1681         my @d = @ext_description;
1682         push @d, $cust_bill_pkg->details if $cust_bill_pkg->recur == 0;
1683         push @b, {
1684           'description'     => $description,
1685           #'pkgpart'         => $part_pkg->pkgpart,
1686           'pkgnum'          => $cust_pkg->pkgnum,
1687           'amount'          => sprintf("%10.2f", $cust_bill_pkg->setup),
1688           'ext_description' => \@d,
1689         };
1690       }
1691
1692       if ( $cust_bill_pkg->recur != 0 ) {
1693         push @b, {
1694           'description'     => "$pkg (" .
1695                                time2str('%x', $cust_bill_pkg->sdate). ' - '.
1696                                time2str('%x', $cust_bill_pkg->edate). ')',
1697           #'pkgpart'         => $part_pkg->pkgpart,
1698           'pkgnum'          => $cust_pkg->pkgnum,
1699           'amount'          => sprintf("%10.2f", $cust_bill_pkg->recur),
1700           'ext_description' => [ @ext_description,
1701                                  $cust_bill_pkg->details,
1702                                ],
1703         };
1704       }
1705
1706     } else { #pkgnum tax or one-shot line item (??)
1707
1708       my $itemdesc = defined $cust_bill_pkg->dbdef_table->column('itemdesc')
1709                      ? ( $cust_bill_pkg->itemdesc || 'Tax' )
1710                      : 'Tax';
1711       if ( $cust_bill_pkg->setup != 0 ) {
1712         push @b, {
1713           'description' => $itemdesc,
1714           'amount'      => sprintf("%10.2f", $cust_bill_pkg->setup),
1715         };
1716       }
1717       if ( $cust_bill_pkg->recur != 0 ) {
1718         push @b, {
1719           'description' => "$itemdesc (".
1720                            time2str("%x", $cust_bill_pkg->sdate). ' - '.
1721                            time2str("%x", $cust_bill_pkg->edate). ')',
1722           'amount'      => sprintf("%10.2f", $cust_bill_pkg->recur),
1723         };
1724       }
1725
1726     }
1727
1728   }
1729
1730   @b;
1731
1732 }
1733
1734 sub _items_credits {
1735   my $self = shift;
1736
1737   my @b;
1738   #credits
1739   foreach ( $self->cust_credited ) {
1740
1741     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
1742
1743     my $reason = $_->cust_credit->reason;
1744     #my $reason = substr($_->cust_credit->reason,0,32);
1745     #$reason .= '...' if length($reason) < length($_->cust_credit->reason);
1746     $reason = " ($reason) " if $reason;
1747     push @b, {
1748       #'description' => 'Credit ref\#'. $_->crednum.
1749       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
1750       #                 $reason,
1751       'description' => 'Credit applied'.
1752                        time2str("%x",$_->cust_credit->_date). $reason,
1753       'amount'      => sprintf("%10.2f",$_->amount),
1754     };
1755   }
1756   #foreach ( @cr_cust_credit ) {
1757   #  push @buf,[
1758   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
1759   #    $money_char. sprintf("%10.2f",$_->credited)
1760   #  ];
1761   #}
1762
1763   @b;
1764
1765 }
1766
1767 sub _items_payments {
1768   my $self = shift;
1769
1770   my @b;
1771   #get & print payments
1772   foreach ( $self->cust_bill_pay ) {
1773
1774     #something more elaborate if $_->amount ne ->cust_pay->paid ?
1775
1776     push @b, {
1777       'description' => "Payment received ".
1778                        time2str("%x",$_->cust_pay->_date ),
1779       'amount'      => sprintf("%10.2f", $_->amount )
1780     };
1781   }
1782
1783   @b;
1784
1785 }
1786
1787 =back
1788
1789 =head1 BUGS
1790
1791 The delete method.
1792
1793 print_text formatting (and some logic :/) is in source, but needs to be
1794 slurped in from a file.  Also number of lines ($=).
1795
1796 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1797 or something similar so the look can be completely customized?)
1798
1799 =head1 SEE ALSO
1800
1801 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1802 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1803 documentation.
1804
1805 =cut
1806
1807 1;
1808