258b32e15374a1ec792acb43bf12285b2241d1ec
[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( $processor );
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( $invoice_lines @buf ); #yuck
10 use Date::Format;
11 use Mail::Internet 1.44;
12 use Mail::Header;
13 use Text::Template;
14 use FS::UID qw( datasrc );
15 use FS::Record qw( qsearch qsearchs );
16 use FS::cust_main;
17 use FS::cust_bill_pkg;
18 use FS::cust_credit;
19 use FS::cust_pay;
20 use FS::cust_pkg;
21 use FS::cust_credit_bill;
22 use FS::cust_pay_batch;
23 use FS::cust_bill_event;
24
25 @ISA = qw( FS::Record );
26
27 #ask FS::UID to run this stuff for us later
28 $FS::UID::callback{'FS::cust_bill'} = sub { 
29
30   $conf = new FS::Conf;
31
32   $money_char = $conf->config('money_char') || '$';  
33
34   $lpr = $conf->config('lpr');
35   $invoice_from = $conf->config('invoice_from');
36   $smtpmachine = $conf->config('smtpmachine');
37
38   if ( $conf->exists('business-onlinepayment') ) {
39     ( $bop_processor,
40       $bop_login,
41       $bop_password,
42       $bop_action,
43       @bop_options
44     ) = $conf->config('business-onlinepayment');
45     $bop_action ||= 'normal authorization';
46     eval "use Business::OnlinePayment";  
47     $processor="Business::OnlinePayment::$bop_processor";
48   }
49
50 };
51
52 =head1 NAME
53
54 FS::cust_bill - Object methods for cust_bill records
55
56 =head1 SYNOPSIS
57
58   use FS::cust_bill;
59
60   $record = new FS::cust_bill \%hash;
61   $record = new FS::cust_bill { 'column' => 'value' };
62
63   $error = $record->insert;
64
65   $error = $new_record->replace($old_record);
66
67   $error = $record->delete;
68
69   $error = $record->check;
70
71   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
72
73   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
74
75   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
76
77   @cust_pay_objects = $cust_bill->cust_pay;
78
79   $tax_amount = $record->tax;
80
81   @lines = $cust_bill->print_text;
82   @lines = $cust_bill->print_text $time;
83
84 =head1 DESCRIPTION
85
86 An FS::cust_bill object represents an invoice; a declaration that a customer
87 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
88 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
89 following fields are currently supported:
90
91 =over 4
92
93 =item invnum - primary key (assigned automatically for new invoices)
94
95 =item custnum - customer (see L<FS::cust_main>)
96
97 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
98 L<Time::Local> and L<Date::Parse> for conversion functions.
99
100 =item charged - amount of this invoice
101
102 =item printed - deprecated
103
104 =item closed - books closed flag, empty or `Y'
105
106 =back
107
108 =head1 METHODS
109
110 =over 4
111
112 =item new HASHREF
113
114 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
115 Invoices are normally created by calling the bill method of a customer object
116 (see L<FS::cust_main>).
117
118 =cut
119
120 sub table { 'cust_bill'; }
121
122 =item insert
123
124 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
125 returns the error, otherwise returns false.
126
127 =item delete
128
129 Currently unimplemented.  I don't remove invoices because there would then be
130 no record you ever posted this invoice (which is bad, no?)
131
132 =cut
133
134 sub delete {
135   my $self = shift;
136   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
137   $self->SUPER::delete(@_);
138 }
139
140 =item replace OLD_RECORD
141
142 Replaces the OLD_RECORD with this one in the database.  If there is an error,
143 returns the error, otherwise returns false.
144
145 Only printed may be changed.  printed is normally updated by calling the
146 collect method of a customer object (see L<FS::cust_main>).
147
148 =cut
149
150 sub replace {
151   my( $new, $old ) = ( shift, shift );
152   return "Can't change custnum!" unless $old->custnum == $new->custnum;
153   #return "Can't change _date!" unless $old->_date eq $new->_date;
154   return "Can't change _date!" unless $old->_date == $new->_date;
155   return "Can't change charged!" unless $old->charged == $new->charged;
156
157   $new->SUPER::replace($old);
158 }
159
160 =item check
161
162 Checks all fields to make sure this is a valid invoice.  If there is an error,
163 returns the error, otherwise returns false.  Called by the insert and replace
164 methods.
165
166 =cut
167
168 sub check {
169   my $self = shift;
170
171   my $error =
172     $self->ut_numbern('invnum')
173     || $self->ut_number('custnum')
174     || $self->ut_numbern('_date')
175     || $self->ut_money('charged')
176     || $self->ut_numbern('printed')
177     || $self->ut_enum('closed', [ '', 'Y' ])
178   ;
179   return $error if $error;
180
181   return "Unknown customer"
182     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
183
184   $self->_date(time) unless $self->_date;
185
186   $self->printed(0) if $self->printed eq '';
187
188   ''; #no error
189 }
190
191 =item previous
192
193 Returns a list consisting of the total previous balance for this customer, 
194 followed by the previous outstanding invoices (as FS::cust_bill objects also).
195
196 =cut
197
198 sub previous {
199   my $self = shift;
200   my $total = 0;
201   my @cust_bill = sort { $a->_date <=> $b->_date }
202     grep { $_->owed != 0 && $_->_date < $self->_date }
203       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
204   ;
205   foreach ( @cust_bill ) { $total += $_->owed; }
206   $total, @cust_bill;
207 }
208
209 =item cust_bill_pkg
210
211 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
212
213 =cut
214
215 sub cust_bill_pkg {
216   my $self = shift;
217   qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum } );
218 }
219
220 =item cust_bill_event
221
222 Returns the completed invoice events (see L<FS::cust_bill_event>) for this
223 invoice.
224
225 =cut
226
227 sub cust_bill_event {
228   my $self = shift;
229   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
230 }
231
232
233 =item cust_main
234
235 Returns the customer (see L<FS::cust_main>) for this invoice.
236
237 =cut
238
239 sub cust_main {
240   my $self = shift;
241   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
242 }
243
244 =item cust_credit
245
246 Depreciated.  See the cust_credited method.
247
248  #Returns a list consisting of the total previous credited (see
249  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
250  #outstanding credits (FS::cust_credit objects).
251
252 =cut
253
254 sub cust_credit {
255   use Carp;
256   croak "FS::cust_bill->cust_credit depreciated; see ".
257         "FS::cust_bill->cust_credit_bill";
258   #my $self = shift;
259   #my $total = 0;
260   #my @cust_credit = sort { $a->_date <=> $b->_date }
261   #  grep { $_->credited != 0 && $_->_date < $self->_date }
262   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
263   #;
264   #foreach (@cust_credit) { $total += $_->credited; }
265   #$total, @cust_credit;
266 }
267
268 =item cust_pay
269
270 Depreciated.  See the cust_bill_pay method.
271
272 #Returns all payments (see L<FS::cust_pay>) for this invoice.
273
274 =cut
275
276 sub cust_pay {
277   use Carp;
278   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
279   #my $self = shift;
280   #sort { $a->_date <=> $b->_date }
281   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
282   #;
283 }
284
285 =item cust_bill_pay
286
287 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
288
289 =cut
290
291 sub cust_bill_pay {
292   my $self = shift;
293   sort { $a->_date <=> $b->_date }
294     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
295 }
296
297 =item cust_credited
298
299 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
300
301 =cut
302
303 sub cust_credited {
304   my $self = shift;
305   sort { $a->_date <=> $b->_date }
306     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
307   ;
308 }
309
310 =item tax
311
312 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
313
314 =cut
315
316 sub tax {
317   my $self = shift;
318   my $total = 0;
319   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
320                                              'pkgnum' => 0 } );
321   foreach (@taxlines) { $total += $_->setup; }
322   $total;
323 }
324
325 =item owed
326
327 Returns the amount owed (still outstanding) on this invoice, which is charged
328 minus all payment applications (see L<FS::cust_bill_pay>) and credit
329 applications (see L<FS::cust_credit_bill>).
330
331 =cut
332
333 sub owed {
334   my $self = shift;
335   my $balance = $self->charged;
336   $balance -= $_->amount foreach ( $self->cust_bill_pay );
337   $balance -= $_->amount foreach ( $self->cust_credited );
338   $balance = sprintf( "%.2f", $balance);
339   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
340   $balance;
341 }
342
343 =item send
344
345 Sends this invoice to the destinations configured for this customer: send
346 emails or print.  See L<FS::cust_main_invoice>.
347
348 =cut
349
350 sub send {
351   my($self,$template) = @_;
352   my @print_text = $self->print_text('', $template);
353   my @invoicing_list = $self->cust_main->invoicing_list;
354
355   if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
356     #false laziness w/FS::cust_pay::delete & fs_signup_server && ::realtime_card
357     #$ENV{SMTPHOSTS} = $smtpmachine;
358     $ENV{MAILADDRESS} = $invoice_from;
359     my $header = new Mail::Header ( [
360       "From: $invoice_from",
361       "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
362       "Sender: $invoice_from",
363       "Reply-To: $invoice_from",
364       "Date: ". time2str("%a, %d %b %Y %X %z", time),
365       "Subject: Invoice",
366     ] );
367     my $message = new Mail::Internet (
368       'Header' => $header,
369       'Body' => [ @print_text ], #( date)
370     );
371     $!=0;
372     $message->smtpsend( Host => $smtpmachine )
373       or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
374         or return "(customer # ". $self->custnum. ") can't send invoice email".
375                   " to ". join(', ', grep { $_ ne 'POST' } @invoicing_list ).
376                   " via server $smtpmachine with SMTP: $!";
377
378   }
379
380   if ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) { #postal
381     open(LPR, "|$lpr")
382       or return "Can't open pipe to $lpr: $!";
383     print LPR @print_text;
384     close LPR
385       or return $! ? "Error closing $lpr: $!"
386                    : "Exit status $? from $lpr";
387   }
388
389   '';
390
391 }
392
393 =item send_csv OPTIONS
394
395 Sends invoice as a CSV data-file to a remote host with the specified protocol.
396
397 Options are:
398
399 protocol - currently only "ftp"
400 server
401 username
402 password
403 dir
404
405 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
406 and YYMMDDHHMMSS is a timestamp.
407
408 The fields of the CSV file is as follows:
409
410 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
411
412 =over 4
413
414 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
415
416 If B<record_type> is C<cust_bill>, this is a primary invoice record.  The
417 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
418 fields are filled in.
419
420 If B<record_type> is C<cust_bill_pkg>, this is a line item record.  Only the
421 first two fields (B<record_type> and B<invnum>) and the last five fields
422 (B<pkg> through B<edate>) are filled in.
423
424 =item invnum - invoice number
425
426 =item custnum - customer number
427
428 =item _date - invoice date
429
430 =item charged - total invoice amount
431
432 =item first - customer first name
433
434 =item last - customer first name
435
436 =item company - company name
437
438 =item address1 - address line 1
439
440 =item address2 - address line 1
441
442 =item city
443
444 =item state
445
446 =item zip
447
448 =item country
449
450 =item pkg - line item description
451
452 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
453
454 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
455
456 =item sdate - start date for recurring fee
457
458 =item edate - end date for recurring fee
459
460 =back
461
462 =cut
463
464 sub send_csv {
465   my($self, %opt) = @_;
466
467   #part one: create file
468
469   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
470   mkdir $spooldir, 0700 unless -d $spooldir;
471
472   my $file = $spooldir. '/'. $self->invnum. time2str('-%Y%m%d%H%M%S.csv', time);
473
474   open(CSV, ">$file") or die "can't open $file: $!";
475
476   eval "use Text::CSV_XS";
477   die $@ if $@;
478
479   my $csv = Text::CSV_XS->new({'always_quote'=>1});
480
481   my $cust_main = $self->cust_main;
482
483   $csv->combine(
484     'cust_bill',
485     $self->invnum,
486     $self->custnum,
487     time2str("%x", $self->_date),
488     sprintf("%.2f", $self->charged),
489     ( map { $cust_main->getfield($_) }
490         qw( first last company address1 address2 city state zip country ) ),
491     map { '' } (1..5),
492   ) or die "can't create csv";
493   print CSV $csv->string. "\n";
494
495   #new charges (false laziness w/print_text)
496   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
497
498     my($pkg, $setup, $recur, $sdate, $edate);
499     if ( $cust_bill_pkg->pkgnum ) {
500     
501       ($pkg, $setup, $recur, $sdate, $edate) = (
502         $cust_bill_pkg->cust_pkg->part_pkg->pkg,
503         ( $cust_bill_pkg->setup != 0
504           ? sprintf("%.2f", $cust_bill_pkg->setup )
505           : '' ),
506         ( $cust_bill_pkg->recur != 0
507           ? sprintf("%.2f", $cust_bill_pkg->recur )
508           : '' ),
509         time2str("%x", $cust_bill_pkg->sdate),
510         time2str("%x", $cust_bill_pkg->edate),
511       );
512
513     } else { #pkgnum Tax
514       next unless $cust_bill_pkg->setup != 0;
515       ($pkg, $setup, $recur, $sdate, $edate) =
516         ( 'Tax', sprintf("%10.2f",$cust_bill_pkg->setup), '', '', '' );
517     }
518
519     $csv->combine(
520       'cust_bill_pkg',
521       $self->invnum,
522       ( map { '' } (1..11) ),
523       ($pkg, $setup, $recur, $sdate, $edate)
524     ) or die "can't create csv";
525     print CSV $csv->string. "\n";
526
527   }
528
529   close CSV or die "can't close CSV: $!";
530
531   #part two: upload it
532
533   my $net;
534   if ( $opt{protocol} eq 'ftp' ) {
535     eval "use Net::FTP;";
536     die $@ if $@;
537     $net = Net::FTP->new($opt{server}) or die @$;
538   } else {
539     die "unknown protocol: $opt{protocol}";
540   }
541
542   $net->login( $opt{username}, $opt{password} )
543     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
544
545   $net->binary or die "can't set binary mode";
546
547   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
548
549   $net->put($file) or die "can't put $file: $!";
550
551   $net->quit;
552
553   unlink $file;
554
555 }
556
557 =item comp
558
559 Pays this invoice with a compliemntary payment.  If there is an error,
560 returns the error, otherwise returns false.
561
562 =cut
563
564 sub comp {
565   my $self = shift;
566   my $cust_pay = new FS::cust_pay ( {
567     'invnum'   => $self->invnum,
568     'paid'     => $self->owed,
569     '_date'    => '',
570     'payby'    => 'COMP',
571     'payinfo'  => $self->cust_main->payinfo,
572     'paybatch' => '',
573   } );
574   $cust_pay->insert;
575 }
576
577 =item realtime_card
578
579 Attempts to pay this invoice with a Business::OnlinePayment realtime gateway.
580 See http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
581 for supproted processors.
582
583 =cut
584
585 sub realtime_card {
586   my $self = shift;
587   my $cust_main = $self->cust_main;
588   my $amount = $self->owed;
589
590   unless ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
591     return "Real-time card processing not enabled (processor $processor)";
592   }
593   my $bop_processor = $1; #hmm?
594
595   my $address = $cust_main->address1;
596   $address .= ", ". $cust_main->address2 if $cust_main->address2;
597
598   #fix exp. date
599   #$cust_main->paydate =~ /^(\d+)\/\d*(\d{2})$/;
600   $cust_main->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
601   my $exp = "$2/$1";
602
603   my($payname, $payfirst, $paylast);
604   if ( $cust_main->payname ) {
605     $payname = $cust_main->payname;
606     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
607       or do {
608               #$dbh->rollback if $oldAutoCommit;
609               return "Illegal payname $payname";
610             };
611     ($payfirst, $paylast) = ($1, $2);
612   } else {
613     $payfirst = $cust_main->getfield('first');
614     $paylast = $cust_main->getfield('last');
615     $payname =  "$payfirst $paylast";
616   }
617
618   my @invoicing_list = grep { $_ ne 'POST' } $cust_main->invoicing_list;
619   if ( $conf->exists('emailinvoiceauto')
620        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
621     push @invoicing_list, $cust_main->all_emails;
622   }
623   my $email = $invoicing_list[0];
624
625   my( $action1, $action2 ) = split(/\s*\,\s*/, $bop_action );
626
627   my $description = 'Internet Services';
628   if ( $conf->exists('business-onlinepayment-description') ) {
629     my $dtempl = $conf->config('business-onlinepayment-description');
630
631     my $agent_obj = $cust_main->agent
632       or die "can't retreive agent for $cust_main (agentnum ".
633              $cust_main->agentnum. ")";
634     my $agent = $agent_obj->agent;
635     my $pkgs = join(', ',
636       map { $_->cust_pkg->part_pkg->pkg }
637         grep { $_->pkgnum } $self->cust_bill_pkg
638     );
639     $description = eval qq("$dtempl");
640
641   }
642   
643   my $transaction =
644     new Business::OnlinePayment( $bop_processor, @bop_options );
645   $transaction->content(
646     'type'           => 'CC',
647     'login'          => $bop_login,
648     'password'       => $bop_password,
649     'action'         => $action1,
650     'description'    => $description,
651     'amount'         => $amount,
652     'invoice_number' => $self->invnum,
653     'customer_id'    => $self->custnum,
654     'last_name'      => $paylast,
655     'first_name'     => $payfirst,
656     'name'           => $payname,
657     'address'        => $address,
658     'city'           => $cust_main->city,
659     'state'          => $cust_main->state,
660     'zip'            => $cust_main->zip,
661     'country'        => $cust_main->country,
662     'card_number'    => $cust_main->payinfo,
663     'expiration'     => $exp,
664     'referer'        => 'http://cleanwhisker.420.am/',
665     'email'          => $email,
666     'phone'          => $cust_main->daytime || $cust_main->night,
667   );
668   $transaction->submit();
669
670   if ( $transaction->is_success() && $action2 ) {
671     my $auth = $transaction->authorization;
672     my $ordernum = $transaction->can('order_number')
673                    ? $transaction->order_number
674                    : '';
675
676     #warn "********* $auth ***********\n";
677     #warn "********* $ordernum ***********\n";
678     my $capture =
679       new Business::OnlinePayment( $bop_processor, @bop_options );
680
681     my %capture = (
682       type           => 'CC',
683       action         => $action2,
684       login          => $bop_login,
685       password       => $bop_password,
686       order_number   => $ordernum,
687       amount         => $amount,
688       authorization  => $auth,
689       description    => $description,
690       card_number    => $cust_main->payinfo,
691       expiration     => $exp,
692     );
693
694     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
695                            transaction_sequence_num local_transaction_date    
696                            local_transaction_time AVS_result_code          )) {
697       $capture{$field} = $transaction->$field() if $transaction->can($field);
698     }
699
700     $capture->content( %capture );
701
702     $capture->submit();
703
704     unless ( $capture->is_success ) {
705       my $e = "Authorization sucessful but capture failed, invnum #".
706               $self->invnum. ': '.  $capture->result_code.
707               ": ". $capture->error_message;
708       warn $e;
709       return $e;
710     }
711
712   }
713
714   if ( $transaction->is_success() ) {
715
716     my $cust_pay = new FS::cust_pay ( {
717        'invnum'   => $self->invnum,
718        'paid'     => $amount,
719        '_date'     => '',
720        'payby'    => 'CARD',
721        'payinfo'  => $cust_main->payinfo,
722        'paybatch' => "$processor:". $transaction->authorization,
723     } );
724     my $error = $cust_pay->insert;
725     if ( $error ) {
726       # gah, even with transactions.
727       my $e = 'WARNING: Card debited but database not updated - '.
728               'error applying payment, invnum #' . $self->invnum.
729               " ($processor): $error";
730       warn $e;
731       return $e;
732     } else {
733       return '';
734     }
735   #} elsif ( $options{'report_badcard'} ) {
736   } else {
737
738     my $perror = "$processor error, invnum #". $self->invnum. ': '.
739                  $transaction->result_code. ": ". $transaction->error_message;
740
741     if ( $conf->exists('emaildecline')
742          && grep { $_ ne 'POST' } $cust_main->invoicing_list
743     ) {
744       my @templ = $conf->config('declinetemplate');
745       my $template = new Text::Template (
746         TYPE   => 'ARRAY',
747         SOURCE => [ map "$_\n", @templ ],
748       ) or return "($perror) can't create template: $Text::Template::ERROR";
749       $template->compile()
750         or return "($perror) can't compile template: $Text::Template::ERROR";
751
752       my $templ_hash = { error => $transaction->error_message };
753
754       #false laziness w/FS::cust_pay::delete & fs_signup_server && ::send
755       $ENV{MAILADDRESS} = $invoice_from;
756       my $header = new Mail::Header ( [
757         "From: $invoice_from",
758         "To: ". join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ),
759         "Sender: $invoice_from",
760         "Reply-To: $invoice_from",
761         "Date: ". time2str("%a, %d %b %Y %X %z", time),
762         "Subject: Your credit card could not be processed",
763       ] );
764       my $message = new Mail::Internet (
765         'Header' => $header,
766         'Body' => [ $template->fill_in(HASH => $templ_hash) ],
767       );
768       $!=0;
769       $message->smtpsend( Host => $smtpmachine )
770         or $message->smtpsend( Host => $smtpmachine, Debug => 1 )
771           or return "($perror) (customer # ". $self->custnum.
772             ") can't send card decline email to ".
773             join(', ', grep { $_ ne 'POST' } $cust_main->invoicing_list ).
774             " via server $smtpmachine with SMTP: $!";
775     }
776   
777     return $perror;
778   }
779
780 }
781
782 =item batch_card
783
784 Adds a payment for this invoice to the pending credit card batch (see
785 L<FS::cust_pay_batch>).
786
787 =cut
788
789 sub batch_card {
790   my $self = shift;
791   my $cust_main = $self->cust_main;
792
793   my $cust_pay_batch = new FS::cust_pay_batch ( {
794     'invnum'   => $self->getfield('invnum'),
795     'custnum'  => $cust_main->getfield('custnum'),
796     'last'     => $cust_main->getfield('last'),
797     'first'    => $cust_main->getfield('first'),
798     'address1' => $cust_main->getfield('address1'),
799     'address2' => $cust_main->getfield('address2'),
800     'city'     => $cust_main->getfield('city'),
801     'state'    => $cust_main->getfield('state'),
802     'zip'      => $cust_main->getfield('zip'),
803     'country'  => $cust_main->getfield('country'),
804     'trancode' => 77,
805     'cardnum'  => $cust_main->getfield('payinfo'),
806     'exp'      => $cust_main->getfield('paydate'),
807     'payname'  => $cust_main->getfield('payname'),
808     'amount'   => $self->owed,
809   } );
810   my $error = $cust_pay_batch->insert;
811   die $error if $error;
812
813   '';
814 }
815
816 =item print_text [TIME];
817
818 Returns an text invoice, as a list of lines.
819
820 TIME an optional value used to control the printing of overdue messages.  The
821 default is now.  It isn't the date of the invoice; that's the `_date' field.
822 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
823 L<Time::Local> and L<Date::Parse> for conversion functions.
824
825 =cut
826
827 sub print_text {
828
829   my( $self, $today, $template ) = @_;
830   $today ||= time;
831 #  my $invnum = $self->invnum;
832   my $cust_main = qsearchs('cust_main', { 'custnum', $self->custnum } );
833   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
834     unless $cust_main->payname;
835
836   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
837 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
838   #my $balance_due = $self->owed + $pr_total - $cr_total;
839   my $balance_due = $self->owed + $pr_total;
840
841   #my @collect = ();
842   #my($description,$amount);
843   @buf = ();
844
845   #previous balance
846   foreach ( @pr_cust_bill ) {
847     push @buf, [
848       "Previous Balance, Invoice #". $_->invnum. 
849                  " (". time2str("%x",$_->_date). ")",
850       $money_char. sprintf("%10.2f",$_->owed)
851     ];
852   }
853   if (@pr_cust_bill) {
854     push @buf,['','-----------'];
855     push @buf,[ 'Total Previous Balance',
856                 $money_char. sprintf("%10.2f",$pr_total ) ];
857     push @buf,['',''];
858   }
859
860   #new charges
861   foreach ( $self->cust_bill_pkg ) {
862
863     if ( $_->pkgnum ) {
864
865       my($cust_pkg)=qsearchs('cust_pkg', { 'pkgnum', $_->pkgnum } );
866       my($part_pkg)=qsearchs('part_pkg',{'pkgpart'=>$cust_pkg->pkgpart});
867       my($pkg)=$part_pkg->pkg;
868
869       if ( $_->setup != 0 ) {
870         push @buf, [ "$pkg Setup", $money_char. sprintf("%10.2f",$_->setup) ];
871         push @buf,
872           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
873       }
874
875       if ( $_->recur != 0 ) {
876         push @buf, [
877           "$pkg (" . time2str("%x",$_->sdate) . " - " .
878                                 time2str("%x",$_->edate) . ")",
879           $money_char. sprintf("%10.2f",$_->recur)
880         ];
881         push @buf,
882           map { [ "  ". $_->[0]. ": ". $_->[1], '' ] } $cust_pkg->labels;
883       }
884
885     } else { #pkgnum Tax
886       push @buf,["Tax", $money_char. sprintf("%10.2f",$_->setup) ] 
887         if $_->setup != 0;
888     }
889   }
890
891   push @buf,['','-----------'];
892   push @buf,['Total New Charges',
893              $money_char. sprintf("%10.2f",$self->charged) ];
894   push @buf,['',''];
895
896   push @buf,['','-----------'];
897   push @buf,['Total Charges',
898              $money_char. sprintf("%10.2f",$self->charged + $pr_total) ];
899   push @buf,['',''];
900
901   #credits
902   foreach ( $self->cust_credited ) {
903
904     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
905
906     my $reason = substr($_->cust_credit->reason,0,32);
907     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
908     $reason = " ($reason) " if $reason;
909     push @buf,[
910       "Credit #". $_->crednum. " (". time2str("%x",$_->cust_credit->_date) .")".
911         $reason,
912       $money_char. sprintf("%10.2f",$_->amount)
913     ];
914   }
915   #foreach ( @cr_cust_credit ) {
916   #  push @buf,[
917   #    "Credit #". $_->crednum. " (" . time2str("%x",$_->_date) .")",
918   #    $money_char. sprintf("%10.2f",$_->credited)
919   #  ];
920   #}
921
922   #get & print payments
923   foreach ( $self->cust_bill_pay ) {
924
925     #something more elaborate if $_->amount ne ->cust_pay->paid ?
926
927     push @buf,[
928       "Payment received ". time2str("%x",$_->cust_pay->_date ),
929       $money_char. sprintf("%10.2f",$_->amount )
930     ];
931   }
932
933   #balance due
934   push @buf,['','-----------'];
935   push @buf,['Balance Due', $money_char. 
936     sprintf("%10.2f", $balance_due ) ];
937
938   #create the template
939   my $templatefile = 'invoice_template';
940   $templatefile .= "_$template" if $template;
941   my @invoice_template = $conf->config($templatefile)
942   or die "cannot load config file $templatefile";
943   $invoice_lines = 0;
944   my $wasfunc = 0;
945   foreach ( grep /invoice_lines\(\d+\)/, @invoice_template ) { #kludgy
946     /invoice_lines\((\d+)\)/;
947     $invoice_lines += $1;
948     $wasfunc=1;
949   }
950   die "no invoice_lines() functions in template?" unless $wasfunc;
951   my $invoice_template = new Text::Template (
952     TYPE   => 'ARRAY',
953     SOURCE => [ map "$_\n", @invoice_template ],
954   ) or die "can't create new Text::Template object: $Text::Template::ERROR";
955   $invoice_template->compile()
956     or die "can't compile template: $Text::Template::ERROR";
957
958   #setup template variables
959   package FS::cust_bill::_template; #!
960   use vars qw( $invnum $date $page $total_pages @address $overdue @buf );
961
962   $invnum = $self->invnum;
963   $date = $self->_date;
964   $page = 1;
965
966   if ( $FS::cust_bill::invoice_lines ) {
967     $total_pages =
968       int( scalar(@FS::cust_bill::buf) / $FS::cust_bill::invoice_lines );
969     $total_pages++
970       if scalar(@FS::cust_bill::buf) % $FS::cust_bill::invoice_lines;
971   } else {
972     $total_pages = 1;
973   }
974
975   #format address (variable for the template)
976   my $l = 0;
977   @address = ( '', '', '', '', '', '' );
978   package FS::cust_bill; #!
979   $FS::cust_bill::_template::address[$l++] =
980     $cust_main->payname.
981       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
982         ? " (P.O. #". $cust_main->payinfo. ")"
983         : ''
984       )
985   ;
986   $FS::cust_bill::_template::address[$l++] = $cust_main->company
987     if $cust_main->company;
988   $FS::cust_bill::_template::address[$l++] = $cust_main->address1;
989   $FS::cust_bill::_template::address[$l++] = $cust_main->address2
990     if $cust_main->address2;
991   $FS::cust_bill::_template::address[$l++] =
992     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
993   $FS::cust_bill::_template::address[$l++] = $cust_main->country
994     unless $cust_main->country eq 'US';
995
996         #  #overdue? (variable for the template)
997         #  $FS::cust_bill::_template::overdue = ( 
998         #    $balance_due > 0
999         #    && $today > $self->_date 
1000         ##    && $self->printed > 1
1001         #    && $self->printed > 0
1002         #  );
1003
1004   #and subroutine for the template
1005
1006   sub FS::cust_bill::_template::invoice_lines {
1007     my $lines = shift or return @buf;
1008     map { 
1009       scalar(@buf) ? shift @buf : [ '', '' ];
1010     }
1011     ( 1 .. $lines );
1012   }
1013
1014
1015   #and fill it in
1016   $FS::cust_bill::_template::page = 1;
1017   my $lines;
1018   my @collect;
1019   while (@buf) {
1020     push @collect, split("\n",
1021       $invoice_template->fill_in( PACKAGE => 'FS::cust_bill::_template' )
1022     );
1023     $FS::cust_bill::_template::page++;
1024   }
1025
1026   map "$_\n", @collect;
1027
1028 }
1029
1030 =back
1031
1032 =head1 VERSION
1033
1034 $Id: cust_bill.pm,v 1.45 2002-09-17 10:21:47 ivan Exp $
1035
1036 =head1 BUGS
1037
1038 The delete method.
1039
1040 print_text formatting (and some logic :/) is in source, but needs to be
1041 slurped in from a file.  Also number of lines ($=).
1042
1043 missing print_ps for a nice postscript copy (maybe HylaFAX-cover-page-style
1044 or something similar so the look can be completely customized?)
1045
1046 =head1 SEE ALSO
1047
1048 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
1049 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
1050 documentation.
1051
1052 =cut
1053
1054 1;
1055