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