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