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