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